Compare commits
109 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a9d315e62 | ||
|
|
c42aa93bf7 | ||
|
|
a7664f03aa | ||
|
|
fa341c9a5a | ||
|
|
a15ab4759e | ||
|
|
b8939763d4 | ||
|
|
51adca8568 | ||
|
|
f646eff048 | ||
|
|
fee0a016d8 | ||
|
|
f040fa5c08 | ||
|
|
7f3c6b4665 | ||
|
|
8b806fe0be | ||
|
|
b37d8c2369 | ||
|
|
b5451e9d3d | ||
|
|
b2235d4c38 | ||
|
|
c8ba5d727e | ||
|
|
a3de44cd0a | ||
|
|
214d9e1c53 | ||
|
|
92900c3f74 | ||
|
|
e17e566daa | ||
|
|
df4e232087 | ||
|
|
828a39331b | ||
|
|
a0223a3eee | ||
|
|
7f6a526b25 | ||
|
|
9983ea25d2 | ||
|
|
47166b937f | ||
|
|
0a09966e81 | ||
|
|
7ec34e934e | ||
|
|
f2b03e7492 | ||
|
|
5208bd62c5 | ||
|
|
d9f0854c27 | ||
|
|
6be125b5cb | ||
|
|
c884c098fd | ||
|
|
f77fe05c92 | ||
|
|
f3bfa8ceba | ||
|
|
ae7d9d87d2 | ||
|
|
1652040c1c | ||
|
|
818b7cdff4 | ||
|
|
48ce359d2d | ||
|
|
e115bf0c6d | ||
|
|
0415b60ba5 | ||
|
|
4570fdb05f | ||
|
|
9d109e7ca9 | ||
|
|
2574553180 | ||
|
|
66e77d50bd | ||
|
|
253bd793d7 | ||
|
|
be30de6728 | ||
|
|
a1455bbb1c | ||
|
|
1ac19ae3e9 | ||
|
|
6e6ca209df | ||
|
|
52699967cd | ||
|
|
146d20ce86 | ||
|
|
b6959b5990 | ||
|
|
06649df8b1 | ||
|
|
0174ed9082 | ||
|
|
adabb281b1 | ||
|
|
63e710d1ab | ||
|
|
509a568446 | ||
|
|
6fd94b53f0 | ||
|
|
164412fa34 | ||
|
|
514ca0810e | ||
|
|
7582f86482 | ||
|
|
4b4c46e5ae | ||
|
|
162e156b33 | ||
|
|
bc7d1971ef | ||
|
|
bbdee92f37 | ||
|
|
cf9e830cc7 | ||
|
|
804e425a87 | ||
|
|
cc21383928 | ||
|
|
413f4efd69 | ||
|
|
de69605eff | ||
|
|
9338ba3525 | ||
|
|
322b6ec615 | ||
|
|
304232d029 | ||
|
|
f80c3bfe07 | ||
|
|
bb0a62fc8b | ||
|
|
5bfdca6cd9 | ||
|
|
447e712a9d | ||
|
|
8bb03f189d | ||
|
|
3b0554cd9b | ||
|
|
858101b0d9 | ||
|
|
a7cf8bee28 | ||
|
|
af1ec7bea9 | ||
|
|
002bf7ef22 | ||
|
|
6919e2336d | ||
|
|
5a5bd22073 | ||
|
|
a726f00f35 | ||
|
|
79297f8a42 | ||
|
|
e3f70ac253 | ||
|
|
838b346fcc | ||
|
|
eba9545ccf | ||
|
|
890ade9495 | ||
|
|
518ef1e0ec | ||
|
|
bdcecfca72 | ||
|
|
1363846ac4 | ||
|
|
30347546a2 | ||
|
|
ac7eb28e91 | ||
|
|
748405473b | ||
|
|
1080390bed | ||
|
|
c48725c7dd | ||
|
|
a5287dbadc | ||
|
|
ee5a3b0dd9 | ||
|
|
3d001541e5 | ||
|
|
b376b229b9 | ||
|
|
33b6203978 | ||
|
|
2d803e009c | ||
|
|
2ddbe38781 | ||
|
|
96181a2b8d | ||
|
|
8308b8eaf2 |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
||||
go get github.com/xtls/xray-core@${{ github.event.inputs.XRAY_CORE_VERSION }} || true
|
||||
gomobile init
|
||||
go mod tidy -v
|
||||
gomobile bind -v -androidapi 19 -ldflags='-s -w' ./
|
||||
gomobile bind -v -androidapi 21 -ldflags='-s -w' ./
|
||||
cp *.aar ${{ github.workspace }}/V2rayNG/app/libs/
|
||||
|
||||
- name: Build APK
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
A V2Ray client for Android, support [Xray core](https://github.com/XTLS/Xray-core) and [v2fly core](https://github.com/v2fly/v2ray-core)
|
||||
|
||||
[](https://developer.android.com/about/versions/lollipop)
|
||||
[](https://kotlinlang.org)
|
||||
[](https://kotlinlang.org)
|
||||
[](https://github.com/2dust/v2rayNG/commits/master)
|
||||
[](https://www.codefactor.io/repository/github/2dust/v2rayng)
|
||||
[](https://github.com/2dust/v2rayNG/releases)
|
||||
|
||||
@@ -11,20 +11,24 @@ android {
|
||||
applicationId = "com.v2ray.ang"
|
||||
minSdk = 21
|
||||
targetSdk = 34
|
||||
versionCode = 562
|
||||
versionName = "1.8.25"
|
||||
versionCode = 585
|
||||
versionName = "1.8.39"
|
||||
multiDexEnabled = true
|
||||
splits.abi {
|
||||
reset()
|
||||
splits {
|
||||
abi {
|
||||
isEnable = true
|
||||
include(
|
||||
"arm64-v8a",
|
||||
"armeabi-v7a",
|
||||
"x86_64",
|
||||
"x86"
|
||||
)
|
||||
isUniversalApk = true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
@@ -50,17 +54,10 @@ android {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
}
|
||||
|
||||
splits {
|
||||
abi {
|
||||
isEnable = true
|
||||
isUniversalApk = false
|
||||
}
|
||||
}
|
||||
|
||||
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,15 +65,12 @@ 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))
|
||||
{
|
||||
if (versionCodes.containsKey(abi)) {
|
||||
output.versionCodeOverride = (1000000 * versionCodes[abi]!!).plus(variant.versionCode)
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
return@forEach
|
||||
}
|
||||
}
|
||||
@@ -86,47 +80,53 @@ android {
|
||||
viewBinding = true
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
packaging {
|
||||
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("androidx.work:work-runtime-ktx:2.8.1")
|
||||
implementation("androidx.work:work-multiprocess:2.8.1")
|
||||
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(libs.work.runtime.ktx)
|
||||
implementation(libs.work.multiprocess)
|
||||
}
|
||||
@@ -10,15 +10,26 @@
|
||||
android:largeScreens="true"
|
||||
android:xlargeScreens="true" />
|
||||
|
||||
<uses-sdk android:minSdkVersion="21" tools:overrideLibrary="com.blacksquircle.ui.editorkit"/>
|
||||
<uses-sdk
|
||||
android:minSdkVersion="21"
|
||||
tools:overrideLibrary="com.blacksquircle.ui.editorkit" />
|
||||
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false"/>
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
|
||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera.autofocus"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.software.leanback"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
|
||||
<!-- https://developer.android.com/about/versions/11/privacy/package-visibility -->
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
<uses-permission
|
||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
@@ -53,12 +64,14 @@
|
||||
android:theme="@style/AppThemeDayNight.NoActionBar">
|
||||
<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" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
@@ -120,8 +133,10 @@
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:scheme="v2rayng" />
|
||||
<data android:host="install-config" />
|
||||
<data android:host="install-sub" />
|
||||
@@ -150,7 +165,8 @@
|
||||
android:value="vpn" />
|
||||
</service>
|
||||
|
||||
<service android:name=".service.V2RayProxyOnlyService"
|
||||
<service
|
||||
android:name=".service.V2RayProxyOnlyService"
|
||||
android:exported="false"
|
||||
android:label="@string/app_name"
|
||||
android:foregroundServiceType="specialUse"
|
||||
@@ -160,10 +176,11 @@
|
||||
android:value="proxy" />
|
||||
</service>
|
||||
|
||||
<service android:name=".service.V2RayTestService"
|
||||
<service
|
||||
android:name=".service.V2RayTestService"
|
||||
android:exported="false"
|
||||
android:process=":RunSoLibV2RayDaemon">
|
||||
</service>
|
||||
android:process=":RunSoLibV2RayDaemon"
|
||||
/>
|
||||
|
||||
<receiver
|
||||
android:exported="true"
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
geosite:cn,
|
||||
geosite:geolocation-cn
|
||||
geosite:cn
|
||||
@@ -81,7 +81,9 @@
|
||||
},
|
||||
{
|
||||
"protocol": "freedom",
|
||||
"settings": {},
|
||||
"settings": {
|
||||
"domainStrategy": "UseIP"
|
||||
},
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
|
||||
package com.v2ray.ang.helper;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
/**
|
||||
* Interface to listen for a move or dismissal event from a {@link ItemTouchHelper.Callback}.
|
||||
@@ -36,7 +36,6 @@ public interface ItemTouchHelperAdapter {
|
||||
* @param fromPosition The start position of the moved item.
|
||||
* @param toPosition Then resolved position of the moved item.
|
||||
* @return True if the item was moved to the new adapter position.
|
||||
*
|
||||
* @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
|
||||
* @see RecyclerView.ViewHolder#getAdapterPosition()
|
||||
*/
|
||||
@@ -52,7 +51,6 @@ public interface ItemTouchHelperAdapter {
|
||||
* adjusting the underlying data to reflect this removal.
|
||||
*
|
||||
* @param position The position of the item dismissed.
|
||||
*
|
||||
* @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
|
||||
* @see RecyclerView.ViewHolder#getAdapterPosition()
|
||||
*/
|
||||
|
||||
@@ -17,9 +17,10 @@
|
||||
package com.v2ray.ang.helper;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
|
||||
@@ -3,10 +3,11 @@ package com.v2ray.ang
|
||||
import android.content.Context
|
||||
import androidx.multidex.MultiDexApplication
|
||||
import androidx.work.Configuration
|
||||
import androidx.work.WorkManager
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.util.Utils
|
||||
|
||||
class AngApplication : MultiDexApplication(), Configuration.Provider {
|
||||
class AngApplication : MultiDexApplication() {
|
||||
companion object {
|
||||
//const val PREF_LAST_VERSION = "pref_last_version"
|
||||
lateinit var application: AngApplication
|
||||
@@ -17,8 +18,9 @@ class AngApplication : MultiDexApplication(), Configuration.Provider {
|
||||
application = this
|
||||
}
|
||||
|
||||
//var firstRun = false
|
||||
// private set
|
||||
private val workManagerConfiguration: Configuration = Configuration.Builder()
|
||||
.setDefaultProcessName("${BuildConfig.APPLICATION_ID}:bg")
|
||||
.build()
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
@@ -34,11 +36,7 @@ class AngApplication : MultiDexApplication(), Configuration.Provider {
|
||||
MMKV.initialize(this)
|
||||
|
||||
Utils.setNightMode(application)
|
||||
}
|
||||
|
||||
override fun getWorkManagerConfiguration(): Configuration {
|
||||
return Configuration.Builder()
|
||||
.setDefaultProcessName("${BuildConfig.APPLICATION_ID}:bg")
|
||||
.build()
|
||||
// Initialize WorkManager with the custom configuration
|
||||
WorkManager.initialize(this, workManagerConfiguration)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
package com.v2ray.ang
|
||||
|
||||
/**
|
||||
*
|
||||
* App Config Const
|
||||
*/
|
||||
|
||||
object AppConfig {
|
||||
|
||||
/** The application's package name. */
|
||||
const val ANG_PACKAGE = BuildConfig.APPLICATION_ID
|
||||
|
||||
/** Directory names used in the app's file system. */
|
||||
const val DIR_ASSETS = "assets"
|
||||
const val DIR_BACKUPS = "backups"
|
||||
|
||||
// legacy
|
||||
/** Legacy configuration keys. */
|
||||
const val ANG_CONFIG = "ang_config"
|
||||
const val PREF_INAPP_BUY_IS_PREMIUM = "pref_inapp_buy_is_premium"
|
||||
|
||||
// Preferences mapped to MMKV
|
||||
/** Preferences mapped to MMKV storage. */
|
||||
const val PREF_SNIFFING_ENABLED = "pref_sniffing_enabled"
|
||||
const val PREF_ROUTE_ONLY_ENABLED = "pref_route_only_enabled"
|
||||
|
||||
const val PREF_PER_APP_PROXY = "pref_per_app_proxy"
|
||||
const val PREF_PER_APP_PROXY_SET = "pref_per_app_proxy_set"
|
||||
const val PREF_BYPASS_APPS = "pref_bypass_apps"
|
||||
@@ -24,70 +24,73 @@ object AppConfig {
|
||||
const val PREF_FAKE_DNS_ENABLED = "pref_fake_dns_enabled"
|
||||
const val PREF_LOCAL_DNS_PORT = "pref_local_dns_port"
|
||||
const val PREF_VPN_DNS = "pref_vpn_dns"
|
||||
|
||||
const val PREF_ROUTING_DOMAIN_STRATEGY = "pref_routing_domain_strategy"
|
||||
const val PREF_ROUTING_MODE = "pref_routing_mode"
|
||||
const val PREF_V2RAY_ROUTING_AGENT = "pref_v2ray_routing_agent"
|
||||
const val PREF_V2RAY_ROUTING_DIRECT = "pref_v2ray_routing_direct"
|
||||
const val PREF_V2RAY_ROUTING_BLOCKED = "pref_v2ray_routing_blocked"
|
||||
const val PREF_ROUTING_CUSTOM = "pref_routing_custom"
|
||||
|
||||
const val PREF_MUX_ENABLED = "pref_mux_enabled"
|
||||
const val PREF_MUX_CONCURRENCY = "pref_mux_concurency"
|
||||
const val PREF_MUX_XUDP_CONCURRENCY = "pref_mux_xudp_concurency"
|
||||
const val PREF_MUX_CONCURRENCY = "pref_mux_concurrency"
|
||||
const val PREF_MUX_XUDP_CONCURRENCY = "pref_mux_xudp_concurrency"
|
||||
const val PREF_MUX_XUDP_QUIC = "pref_mux_xudp_quic"
|
||||
|
||||
const val PREF_FRAGMENT_ENABLED = "pref_fragment_enabled"
|
||||
const val PREF_FRAGMENT_PACKETS = "pref_fragment_packets"
|
||||
const val PREF_FRAGMENT_LENGTH = "pref_fragment_length"
|
||||
const val PREF_FRAGMENT_INTERVAL = "pref_fragment_interval"
|
||||
|
||||
const val SUBSCRIPTION_AUTO_UPDATE = "pref_auto_update_subscription"
|
||||
const val SUBSCRIPTION_AUTO_UPDATE_INTERVAL = "pref_auto_update_interval"
|
||||
const val SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL = "1440" // 24 hours
|
||||
const val SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL = "1440" // Default is 24 hours
|
||||
const val SUBSCRIPTION_UPDATE_TASK_NAME = "subscription_updater"
|
||||
|
||||
const val PREF_SPEED_ENABLED = "pref_speed_enabled"
|
||||
const val PREF_CONFIRM_REMOVE = "pref_confirm_remove"
|
||||
const val PREF_START_SCAN_IMMEDIATE = "pref_start_scan_immediate"
|
||||
const val PREF_LANGUAGE = "pref_language"
|
||||
const val PREF_UI_MODE_NIGHT = "pref_ui_mode_night"
|
||||
|
||||
const val PREF_PREFER_IPV6 = "pref_prefer_ipv6"
|
||||
const val PREF_PROXY_SHARING = "pref_proxy_sharing_enabled"
|
||||
const val PREF_ALLOW_INSECURE = "pref_allow_insecure"
|
||||
const val PREF_SOCKS_PORT = "pref_socks_port"
|
||||
const val PREF_HTTP_PORT = "pref_http_port"
|
||||
|
||||
const val PREF_REMOTE_DNS = "pref_remote_dns"
|
||||
const val PREF_DOMESTIC_DNS = "pref_domestic_dns"
|
||||
const val PREF_DELAY_TEST_URL = "pref_delay_test_url"
|
||||
const val PREF_LOGLEVEL = "pref_core_loglevel"
|
||||
const val PREF_MODE = "pref_mode"
|
||||
|
||||
/** Cache keys. */
|
||||
const val CACHE_SUBSCRIPTION_ID = "cache_subscription_id"
|
||||
const val CACHE_KEYWORD_FILTER = "cache_keyword_filter"
|
||||
|
||||
//Preferences mapped to MMKV End
|
||||
|
||||
/** Protocol identifiers. */
|
||||
const val PROTOCOL_HTTP: String = "http://"
|
||||
const val PROTOCOL_HTTPS: String = "https://"
|
||||
const val PROTOCOL_FREEDOM: String = "freedom"
|
||||
|
||||
/** Broadcast actions. */
|
||||
const val BROADCAST_ACTION_SERVICE = "com.v2ray.ang.action.service"
|
||||
const val BROADCAST_ACTION_ACTIVITY = "com.v2ray.ang.action.activity"
|
||||
const val BROADCAST_ACTION_WIDGET_CLICK = "com.v2ray.ang.action.widget.click"
|
||||
|
||||
/** Tasker extras. */
|
||||
const val TASKER_EXTRA_BUNDLE = "com.twofortyfouram.locale.intent.extra.BUNDLE"
|
||||
const val TASKER_EXTRA_STRING_BLURB = "com.twofortyfouram.locale.intent.extra.BLURB"
|
||||
const val TASKER_EXTRA_BUNDLE_SWITCH = "tasker_extra_bundle_switch"
|
||||
const val TASKER_EXTRA_BUNDLE_GUID = "tasker_extra_bundle_guid"
|
||||
const val TASKER_DEFAULT_GUID = "Default"
|
||||
|
||||
/** Tags for different proxy modes. */
|
||||
const val TAG_PROXY = "proxy"
|
||||
const val TAG_DIRECT = "direct"
|
||||
const val TAG_BLOCKED = "block"
|
||||
const val TAG_FRAGMENT = "fragment"
|
||||
|
||||
/** Network-related constants. */
|
||||
const val UPLINK = "uplink"
|
||||
const val DOWNLINK = "downlink"
|
||||
|
||||
/** URLs for various resources. */
|
||||
const val androidpackagenamelistUrl =
|
||||
"https://raw.githubusercontent.com/2dust/androidpackagenamelist/master/proxy.txt"
|
||||
const val v2rayCustomRoutingListUrl =
|
||||
@@ -100,11 +103,14 @@ 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"
|
||||
|
||||
/** DNS server addresses. */
|
||||
const val DNS_PROXY = "1.1.1.1"
|
||||
const val DNS_DIRECT = "223.5.5.5"
|
||||
const val DNS_VPN = "1.1.1.1"
|
||||
|
||||
/** Ports and addresses for various services. */
|
||||
const val PORT_LOCAL_DNS = "10853"
|
||||
const val PORT_SOCKS = "10808"
|
||||
const val PORT_HTTP = "10809"
|
||||
@@ -112,6 +118,7 @@ object AppConfig {
|
||||
const val WIREGUARD_LOCAL_ADDRESS_V6 = "2606:4700:110:8f81:d551:a0:532e:a2b3/128"
|
||||
const val WIREGUARD_LOCAL_MTU = "1420"
|
||||
|
||||
/** Message constants for communication. */
|
||||
const val MSG_REGISTER_CLIENT = 1
|
||||
const val MSG_STATE_RUNNING = 11
|
||||
const val MSG_STATE_NOT_RUNNING = 12
|
||||
@@ -127,4 +134,19 @@ object AppConfig {
|
||||
const val MSG_MEASURE_CONFIG = 7
|
||||
const val MSG_MEASURE_CONFIG_SUCCESS = 71
|
||||
const val MSG_MEASURE_CONFIG_CANCEL = 72
|
||||
|
||||
/** Notification channel IDs and names. */
|
||||
const val RAY_NG_CHANNEL_ID = "RAY_NG_M_CH_ID"
|
||||
const val RAY_NG_CHANNEL_NAME = "V2rayNG Background Service"
|
||||
const val SUBSCRIPTION_UPDATE_CHANNEL = "subscription_update_channel"
|
||||
const val SUBSCRIPTION_UPDATE_CHANNEL_NAME = "Subscription Update Service"
|
||||
|
||||
/** Protocols Scheme **/
|
||||
const val VMESS = "vmess://"
|
||||
const val CUSTOM = ""
|
||||
const val SHADOWSOCKS = "ss://"
|
||||
const val SOCKS = "socks://"
|
||||
const val VLESS = "vless://"
|
||||
const val TROJAN = "trojan://"
|
||||
const val WIREGUARD = "wireguard://"
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -2,8 +2,10 @@ package com.v2ray.ang.dto
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
|
||||
data class AppInfo(val appName: String,
|
||||
data class AppInfo(
|
||||
val appName: String,
|
||||
val packageName: String,
|
||||
val appIcon: Drawable,
|
||||
val isSystemApp: Boolean,
|
||||
var isSelected: Int)
|
||||
var isSelected: Int
|
||||
)
|
||||
@@ -1,13 +1,16 @@
|
||||
package com.v2ray.ang.dto
|
||||
|
||||
import com.v2ray.ang.AppConfig
|
||||
|
||||
|
||||
enum class EConfigType(val value: Int, val protocolScheme: String) {
|
||||
VMESS(1, "vmess://"),
|
||||
CUSTOM(2, ""),
|
||||
SHADOWSOCKS(3, "ss://"),
|
||||
SOCKS(4, "socks://"),
|
||||
VLESS(5, "vless://"),
|
||||
TROJAN(6, "trojan://"),
|
||||
WIREGUARD(7, "wireguard://");
|
||||
VMESS(1, AppConfig.VMESS),
|
||||
CUSTOM(2, AppConfig.CUSTOM),
|
||||
SHADOWSOCKS(3, AppConfig.SHADOWSOCKS),
|
||||
SOCKS(4, AppConfig.SOCKS),
|
||||
VLESS(5, AppConfig.VLESS),
|
||||
TROJAN(6, AppConfig.TROJAN),
|
||||
WIREGUARD(7, AppConfig.WIREGUARD);
|
||||
|
||||
companion object {
|
||||
fun fromInt(value: Int) = values().firstOrNull { it.value == value }
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.v2ray.ang.dto
|
||||
|
||||
data class ProfileItem(
|
||||
val configType: EConfigType,
|
||||
var subscriptionId: String = "",
|
||||
var remarks: String = "",
|
||||
var server: String?,
|
||||
var serverPort: Int?,
|
||||
)
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.v2ray.ang.dto
|
||||
|
||||
import com.v2ray.ang.AppConfig.TAG_PROXY
|
||||
import com.v2ray.ang.AppConfig.TAG_BLOCKED
|
||||
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
||||
import com.v2ray.ang.AppConfig.TAG_PROXY
|
||||
import com.v2ray.ang.util.Utils
|
||||
|
||||
data class ServerConfig(
|
||||
@@ -23,19 +23,31 @@ data class ServerConfig(
|
||||
outboundBean = V2rayConfig.OutboundBean(
|
||||
protocol = configType.name.lowercase(),
|
||||
settings = V2rayConfig.OutboundBean.OutSettingsBean(
|
||||
vnext = listOf(V2rayConfig.OutboundBean.OutSettingsBean.VnextBean(
|
||||
users = listOf(V2rayConfig.OutboundBean.OutSettingsBean.VnextBean.UsersBean())))),
|
||||
streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean()))
|
||||
vnext = listOf(
|
||||
V2rayConfig.OutboundBean.OutSettingsBean.VnextBean(
|
||||
users = listOf(V2rayConfig.OutboundBean.OutSettingsBean.VnextBean.UsersBean())
|
||||
)
|
||||
)
|
||||
),
|
||||
streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean()
|
||||
)
|
||||
)
|
||||
|
||||
EConfigType.CUSTOM ->
|
||||
return ServerConfig(configType = configType)
|
||||
|
||||
EConfigType.SHADOWSOCKS, EConfigType.SOCKS, EConfigType.TROJAN ->
|
||||
return ServerConfig(
|
||||
configType = configType,
|
||||
outboundBean = V2rayConfig.OutboundBean(
|
||||
protocol = configType.name.lowercase(),
|
||||
settings = V2rayConfig.OutboundBean.OutSettingsBean(
|
||||
servers = listOf(V2rayConfig.OutboundBean.OutSettingsBean.ServersBean())),
|
||||
streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean()))
|
||||
servers = listOf(V2rayConfig.OutboundBean.OutSettingsBean.ServersBean())
|
||||
),
|
||||
streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean()
|
||||
)
|
||||
)
|
||||
|
||||
EConfigType.WIREGUARD ->
|
||||
return ServerConfig(
|
||||
configType = configType,
|
||||
@@ -44,7 +56,9 @@ data class ServerConfig(
|
||||
settings = V2rayConfig.OutboundBean.OutSettingsBean(
|
||||
secretKey = "",
|
||||
peers = listOf(V2rayConfig.OutboundBean.OutSettingsBean.WireGuardBean())
|
||||
)))
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
package com.v2ray.ang.dto
|
||||
|
||||
data class ServersCache(val guid: String,
|
||||
val config: ServerConfig)
|
||||
data class ServersCache(
|
||||
val guid: String,
|
||||
val profile: ProfileItem
|
||||
)
|
||||
@@ -24,7 +24,8 @@ data class V2rayConfig(
|
||||
var fakedns: Any? = null,
|
||||
val browserForwarder: Any? = null,
|
||||
var observatory: Any? = null,
|
||||
var burstObservatory: Any? = null) {
|
||||
var burstObservatory: Any? = null
|
||||
) {
|
||||
companion object {
|
||||
const val DEFAULT_PORT = 443
|
||||
const val DEFAULT_SECURITY = "auto"
|
||||
@@ -36,10 +37,12 @@ data class V2rayConfig(
|
||||
const val HTTP = "http"
|
||||
}
|
||||
|
||||
data class LogBean(val access: String,
|
||||
data class LogBean(
|
||||
val access: String,
|
||||
val error: String,
|
||||
var loglevel: String?,
|
||||
val dnsLog: Boolean? = null)
|
||||
val dnsLog: Boolean? = null
|
||||
)
|
||||
|
||||
data class InboundBean(
|
||||
var tag: String,
|
||||
@@ -49,31 +52,40 @@ data class V2rayConfig(
|
||||
val settings: Any? = null,
|
||||
val sniffing: SniffingBean?,
|
||||
val streamSettings: Any? = null,
|
||||
val allocate: Any? = null) {
|
||||
val allocate: Any? = null
|
||||
) {
|
||||
|
||||
data class InSettingsBean(val auth: String? = null,
|
||||
data class InSettingsBean(
|
||||
val auth: String? = null,
|
||||
val udp: Boolean? = null,
|
||||
val userLevel: Int? = null,
|
||||
val address: String? = null,
|
||||
val port: Int? = null,
|
||||
val network: String? = null)
|
||||
val network: String? = null
|
||||
)
|
||||
|
||||
data class SniffingBean(var enabled: Boolean,
|
||||
data class SniffingBean(
|
||||
var enabled: Boolean,
|
||||
val destOverride: ArrayList<String>,
|
||||
val metadataOnly: Boolean? = null,
|
||||
var routeOnly: Boolean? = null)
|
||||
var routeOnly: Boolean? = null
|
||||
)
|
||||
}
|
||||
|
||||
data class OutboundBean(var tag: String = "proxy",
|
||||
data class OutboundBean(
|
||||
var tag: String = "proxy",
|
||||
var protocol: String,
|
||||
var settings: OutSettingsBean? = null,
|
||||
var streamSettings: StreamSettingsBean? = null,
|
||||
val proxySettings: Any? = null,
|
||||
val sendThrough: String? = null,
|
||||
var mux: MuxBean? = MuxBean(false)) {
|
||||
var mux: MuxBean? = MuxBean(false)
|
||||
) {
|
||||
|
||||
data class OutSettingsBean(var vnext: List<VnextBean>? = null,
|
||||
data class OutSettingsBean(
|
||||
var vnext: List<VnextBean>? = null,
|
||||
var fragment: FragmentBean? = null,
|
||||
var noise: NoiseBean? = null,
|
||||
var servers: List<ServersBean>? = null,
|
||||
/*Blackhole*/
|
||||
var response: Response? = null,
|
||||
@@ -94,23 +106,35 @@ data class V2rayConfig(
|
||||
var mtu: Int? = null
|
||||
) {
|
||||
|
||||
data class VnextBean(var address: String = "",
|
||||
data class VnextBean(
|
||||
var address: String = "",
|
||||
var port: Int = DEFAULT_PORT,
|
||||
var users: List<UsersBean>) {
|
||||
var users: List<UsersBean>
|
||||
) {
|
||||
|
||||
data class UsersBean(var id: String = "",
|
||||
data class UsersBean(
|
||||
var id: String = "",
|
||||
var alterId: Int? = null,
|
||||
var security: String = DEFAULT_SECURITY,
|
||||
var level: Int = DEFAULT_LEVEL,
|
||||
var encryption: String = "",
|
||||
var flow: String = "")
|
||||
var flow: String = ""
|
||||
)
|
||||
}
|
||||
|
||||
data class FragmentBean(var packets: String? = null,
|
||||
data class FragmentBean(
|
||||
var packets: String? = null,
|
||||
var length: String? = null,
|
||||
var interval: String? = null)
|
||||
var interval: String? = null
|
||||
)
|
||||
|
||||
data class ServersBean(var address: String = "",
|
||||
data class NoiseBean(
|
||||
var packet: String? = null,
|
||||
var delay: String? = null
|
||||
)
|
||||
|
||||
data class ServersBean(
|
||||
var address: String = "",
|
||||
var method: String = "chacha20-poly1305",
|
||||
var ota: Boolean = false,
|
||||
var password: String = "",
|
||||
@@ -119,26 +143,33 @@ data class V2rayConfig(
|
||||
val email: String? = null,
|
||||
var flow: String? = null,
|
||||
val ivCheck: Boolean? = null,
|
||||
var users: List<SocksUsersBean>? = null) {
|
||||
var users: List<SocksUsersBean>? = null
|
||||
) {
|
||||
|
||||
|
||||
data class SocksUsersBean(var user: String = "",
|
||||
data class SocksUsersBean(
|
||||
var user: String = "",
|
||||
var pass: String = "",
|
||||
var level: Int = DEFAULT_LEVEL)
|
||||
var level: Int = DEFAULT_LEVEL
|
||||
)
|
||||
}
|
||||
|
||||
data class Response(var type: String)
|
||||
|
||||
data class WireGuardBean(var publicKey: String = "",
|
||||
var endpoint: String = "")
|
||||
data class WireGuardBean(
|
||||
var publicKey: String = "",
|
||||
var endpoint: String = ""
|
||||
)
|
||||
}
|
||||
|
||||
data class StreamSettingsBean(var network: String = DEFAULT_NETWORK,
|
||||
data class StreamSettingsBean(
|
||||
var network: String = DEFAULT_NETWORK,
|
||||
var security: String = "",
|
||||
var tcpSettings: TcpSettingsBean? = null,
|
||||
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,
|
||||
@@ -148,27 +179,36 @@ data class V2rayConfig(
|
||||
var sockopt: SockoptBean? = null
|
||||
) {
|
||||
|
||||
data class TcpSettingsBean(var header: HeaderBean = HeaderBean(),
|
||||
val acceptProxyProtocol: Boolean? = null) {
|
||||
data class HeaderBean(var type: String = "none",
|
||||
data class TcpSettingsBean(
|
||||
var header: HeaderBean = HeaderBean(),
|
||||
val acceptProxyProtocol: Boolean? = null
|
||||
) {
|
||||
data class HeaderBean(
|
||||
var type: String = "none",
|
||||
var request: RequestBean? = null,
|
||||
var response: Any? = null) {
|
||||
data class RequestBean(var path: List<String> = ArrayList(),
|
||||
var response: Any? = null
|
||||
) {
|
||||
data class RequestBean(
|
||||
var path: List<String> = ArrayList(),
|
||||
var headers: HeadersBean = HeadersBean(),
|
||||
val version: String? = null,
|
||||
val method: String? = null) {
|
||||
data class HeadersBean(var Host: List<String> = ArrayList(),
|
||||
val method: String? = null
|
||||
) {
|
||||
data class HeadersBean(
|
||||
var Host: List<String>? = ArrayList(),
|
||||
@SerializedName("User-Agent")
|
||||
val userAgent: List<String>? = null,
|
||||
@SerializedName("Accept-Encoding")
|
||||
val acceptEncoding: List<String>? = null,
|
||||
val Connection: List<String>? = null,
|
||||
val Pragma: String? = null)
|
||||
val Pragma: String? = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class KcpSettingsBean(var mtu: Int = 1350,
|
||||
data class KcpSettingsBean(
|
||||
var mtu: Int = 1350,
|
||||
var tti: Int = 50,
|
||||
var uplinkCapacity: Int = 12,
|
||||
var downlinkCapacity: Int = 100,
|
||||
@@ -176,33 +216,50 @@ data class V2rayConfig(
|
||||
var readBufferSize: Int = 1,
|
||||
var writeBufferSize: Int = 1,
|
||||
var header: HeaderBean = HeaderBean(),
|
||||
var seed: String? = null) {
|
||||
var seed: String? = null
|
||||
) {
|
||||
data class HeaderBean(var type: String = "none")
|
||||
}
|
||||
|
||||
data class WsSettingsBean(var path: String = "",
|
||||
data class WsSettingsBean(
|
||||
var path: String = "",
|
||||
var headers: HeadersBean = HeadersBean(),
|
||||
val maxEarlyData: Int? = null,
|
||||
val useBrowserForwarding: Boolean? = null,
|
||||
val acceptProxyProtocol: Boolean? = null) {
|
||||
val acceptProxyProtocol: Boolean? = null
|
||||
) {
|
||||
data class HeadersBean(var Host: String = "")
|
||||
}
|
||||
|
||||
data class HttpupgradeSettingsBean(var path: String = "",
|
||||
data class HttpupgradeSettingsBean(
|
||||
var path: String = "",
|
||||
var host: String = "",
|
||||
val acceptProxyProtocol: Boolean? = null)
|
||||
val acceptProxyProtocol: Boolean? = null
|
||||
)
|
||||
|
||||
data class HttpSettingsBean(var host: List<String> = ArrayList(),
|
||||
var path: String = "")
|
||||
data class SplithttpSettingsBean(
|
||||
var path: String = "",
|
||||
var host: String = "",
|
||||
val maxUploadSize: Int? = null,
|
||||
val maxConcurrentUploads: Int? = null
|
||||
)
|
||||
|
||||
data class SockoptBean(var TcpNoDelay: Boolean? = null,
|
||||
data class HttpSettingsBean(
|
||||
var host: List<String> = ArrayList(),
|
||||
var path: String = ""
|
||||
)
|
||||
|
||||
data class SockoptBean(
|
||||
var TcpNoDelay: Boolean? = null,
|
||||
var tcpKeepAliveIdle: Int? = null,
|
||||
var tcpFastOpen: Boolean? = null,
|
||||
var tproxy: String? = null,
|
||||
var mark: Int? = null,
|
||||
var dialerProxy: String? = null)
|
||||
var dialerProxy: String? = null
|
||||
)
|
||||
|
||||
data class TlsSettingsBean(var allowInsecure: Boolean = false,
|
||||
data class TlsSettingsBean(
|
||||
var allowInsecure: Boolean = false,
|
||||
var serverName: String = "",
|
||||
val alpn: List<String>? = null,
|
||||
val minVersion: String? = null,
|
||||
@@ -217,24 +274,30 @@ data class V2rayConfig(
|
||||
val show: Boolean = false,
|
||||
var publicKey: String? = null,
|
||||
var shortId: String? = null,
|
||||
var spiderX: String? = null)
|
||||
var spiderX: String? = null
|
||||
)
|
||||
|
||||
data class QuicSettingBean(var security: String = "none",
|
||||
data class QuicSettingBean(
|
||||
var security: String = "none",
|
||||
var key: String = "",
|
||||
var header: HeaderBean = HeaderBean()) {
|
||||
var header: HeaderBean = HeaderBean()
|
||||
) {
|
||||
data class HeaderBean(var type: String = "none")
|
||||
}
|
||||
|
||||
data class GrpcSettingsBean(var serviceName: String = "",
|
||||
data class GrpcSettingsBean(
|
||||
var serviceName: String = "",
|
||||
var authority: String? = null,
|
||||
var multiMode: Boolean? = null,
|
||||
var idle_timeout: Int? = null,
|
||||
var health_check_timeout: Int? = null
|
||||
)
|
||||
|
||||
fun populateTransportSettings(transport: String, headerType: String?, host: String?, path: String?, seed: String?,
|
||||
fun populateTransportSettings(
|
||||
transport: String, headerType: String?, host: String?, path: String?, seed: String?,
|
||||
quicSecurity: String?, key: String?, mode: String?, serviceName: String?,
|
||||
authority: String?): String {
|
||||
authority: String?
|
||||
): String {
|
||||
var sni = ""
|
||||
network = transport
|
||||
when (network) {
|
||||
@@ -244,17 +307,18 @@ data class V2rayConfig(
|
||||
tcpSetting.header.type = HTTP
|
||||
if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(path)) {
|
||||
val requestObj = TcpSettingsBean.HeaderBean.RequestBean()
|
||||
requestObj.headers.Host = (host ?: "").split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||
requestObj.path = (path ?: "").split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||
requestObj.headers.Host = (host.orEmpty()).split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||
requestObj.path = (path.orEmpty()).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"
|
||||
sni = host ?: ""
|
||||
sni = host.orEmpty()
|
||||
}
|
||||
tcpSettings = tcpSetting
|
||||
}
|
||||
|
||||
"kcp" -> {
|
||||
val kcpsetting = KcpSettingsBean()
|
||||
kcpsetting.header.type = headerType ?: "none"
|
||||
@@ -265,51 +329,66 @@ data class V2rayConfig(
|
||||
}
|
||||
kcpSettings = kcpsetting
|
||||
}
|
||||
|
||||
"ws" -> {
|
||||
val wssetting = WsSettingsBean()
|
||||
wssetting.headers.Host = host ?: ""
|
||||
wssetting.headers.Host = host.orEmpty()
|
||||
sni = wssetting.headers.Host
|
||||
wssetting.path = path ?: "/"
|
||||
wsSettings = wssetting
|
||||
}
|
||||
|
||||
"httpupgrade" -> {
|
||||
val httpupgradeSetting = HttpupgradeSettingsBean()
|
||||
httpupgradeSetting.host = host ?: ""
|
||||
httpupgradeSetting.host = host.orEmpty()
|
||||
sni = httpupgradeSetting.host
|
||||
httpupgradeSetting.path = path ?: "/"
|
||||
httpupgradeSettings = httpupgradeSetting
|
||||
}
|
||||
|
||||
"splithttp" -> {
|
||||
val splithttpSetting = SplithttpSettingsBean()
|
||||
splithttpSetting.host = host.orEmpty()
|
||||
sni = splithttpSetting.host
|
||||
splithttpSetting.path = path ?: "/"
|
||||
splithttpSettings = splithttpSetting
|
||||
}
|
||||
|
||||
"h2", "http" -> {
|
||||
network = "h2"
|
||||
val h2Setting = HttpSettingsBean()
|
||||
h2Setting.host = (host ?: "").split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||
h2Setting.host = (host.orEmpty()).split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||
sni = h2Setting.host.getOrNull(0) ?: sni
|
||||
h2Setting.path = path ?: "/"
|
||||
httpSettings = h2Setting
|
||||
}
|
||||
|
||||
"quic" -> {
|
||||
val quicsetting = QuicSettingBean()
|
||||
quicsetting.security = quicSecurity ?: "none"
|
||||
quicsetting.key = key ?: ""
|
||||
quicsetting.key = key.orEmpty()
|
||||
quicsetting.header.type = headerType ?: "none"
|
||||
quicSettings = quicsetting
|
||||
}
|
||||
|
||||
"grpc" -> {
|
||||
val grpcSetting = GrpcSettingsBean()
|
||||
grpcSetting.multiMode = mode == "multi"
|
||||
grpcSetting.serviceName = serviceName ?: ""
|
||||
grpcSetting.authority = authority ?: ""
|
||||
grpcSetting.serviceName = serviceName.orEmpty()
|
||||
grpcSetting.authority = authority.orEmpty()
|
||||
grpcSetting.idle_timeout = 60
|
||||
grpcSetting.health_check_timeout = 20
|
||||
sni = authority ?: ""
|
||||
sni = authority.orEmpty()
|
||||
grpcSettings = grpcSetting
|
||||
}
|
||||
}
|
||||
return sni
|
||||
}
|
||||
|
||||
fun populateTlsSettings(streamSecurity: String, allowInsecure: Boolean, sni: String, fingerprint: String?, alpns: String?,
|
||||
publicKey: String?, shortId: String?, spiderX: String?) {
|
||||
fun populateTlsSettings(
|
||||
streamSecurity: String, allowInsecure: Boolean, sni: String, fingerprint: String?, alpns: String?,
|
||||
publicKey: String?, shortId: String?, spiderX: String?
|
||||
) {
|
||||
security = streamSecurity
|
||||
val tlsSetting = TlsSettingsBean(
|
||||
allowInsecure = allowInsecure,
|
||||
@@ -330,18 +409,22 @@ data class V2rayConfig(
|
||||
}
|
||||
}
|
||||
|
||||
data class MuxBean(var enabled: Boolean,
|
||||
data class MuxBean(
|
||||
var enabled: Boolean,
|
||||
var concurrency: Int = 8,
|
||||
var xudpConcurrency: Int = 8,
|
||||
var xudpProxyUDP443: String = "",)
|
||||
var xudpProxyUDP443: String = "",
|
||||
)
|
||||
|
||||
fun getServerAddress(): String? {
|
||||
if (protocol.equals(EConfigType.VMESS.name, true)
|
||||
|| protocol.equals(EConfigType.VLESS.name, true)) {
|
||||
|| protocol.equals(EConfigType.VLESS.name, true)
|
||||
) {
|
||||
return settings?.vnext?.get(0)?.address
|
||||
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
||||
|| protocol.equals(EConfigType.SOCKS.name, true)
|
||||
|| protocol.equals(EConfigType.TROJAN.name, true)) {
|
||||
|| protocol.equals(EConfigType.TROJAN.name, true)
|
||||
) {
|
||||
return settings?.servers?.get(0)?.address
|
||||
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
||||
return settings?.peers?.get(0)?.endpoint?.substringBeforeLast(":")
|
||||
@@ -351,11 +434,13 @@ data class V2rayConfig(
|
||||
|
||||
fun getServerPort(): Int? {
|
||||
if (protocol.equals(EConfigType.VMESS.name, true)
|
||||
|| protocol.equals(EConfigType.VLESS.name, true)) {
|
||||
|| protocol.equals(EConfigType.VLESS.name, true)
|
||||
) {
|
||||
return settings?.vnext?.get(0)?.port
|
||||
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
||||
|| protocol.equals(EConfigType.SOCKS.name, true)
|
||||
|| protocol.equals(EConfigType.TROJAN.name, true)) {
|
||||
|| protocol.equals(EConfigType.TROJAN.name, true)
|
||||
) {
|
||||
return settings?.servers?.get(0)?.port
|
||||
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
||||
return settings?.peers?.get(0)?.endpoint?.substringAfterLast(":")?.toInt()
|
||||
@@ -365,10 +450,12 @@ data class V2rayConfig(
|
||||
|
||||
fun getPassword(): String? {
|
||||
if (protocol.equals(EConfigType.VMESS.name, true)
|
||||
|| protocol.equals(EConfigType.VLESS.name, true)) {
|
||||
|| protocol.equals(EConfigType.VLESS.name, true)
|
||||
) {
|
||||
return settings?.vnext?.get(0)?.users?.get(0)?.id
|
||||
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
||||
|| protocol.equals(EConfigType.TROJAN.name, true)) {
|
||||
|| protocol.equals(EConfigType.TROJAN.name, true)
|
||||
) {
|
||||
return settings?.servers?.get(0)?.password
|
||||
} else if (protocol.equals(EConfigType.SOCKS.name, true)) {
|
||||
return settings?.servers?.get(0)?.users?.get(0)?.pass
|
||||
@@ -391,51 +478,82 @@ data class V2rayConfig(
|
||||
if (protocol.equals(EConfigType.VMESS.name, true)
|
||||
|| protocol.equals(EConfigType.VLESS.name, true)
|
||||
|| protocol.equals(EConfigType.TROJAN.name, true)
|
||||
|| protocol.equals(EConfigType.SHADOWSOCKS.name, true)) {
|
||||
|| protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
||||
) {
|
||||
val transport = streamSettings?.network ?: return null
|
||||
return when (transport) {
|
||||
"tcp" -> {
|
||||
val tcpSetting = streamSettings?.tcpSettings ?: return null
|
||||
listOf(tcpSetting.header.type,
|
||||
listOf(
|
||||
tcpSetting.header.type,
|
||||
tcpSetting.header.request?.headers?.Host?.joinToString().orEmpty(),
|
||||
tcpSetting.header.request?.path?.joinToString().orEmpty())
|
||||
tcpSetting.header.request?.path?.joinToString().orEmpty()
|
||||
)
|
||||
}
|
||||
|
||||
"kcp" -> {
|
||||
val kcpSetting = streamSettings?.kcpSettings ?: return null
|
||||
listOf(kcpSetting.header.type,
|
||||
listOf(
|
||||
kcpSetting.header.type,
|
||||
"",
|
||||
kcpSetting.seed.orEmpty())
|
||||
kcpSetting.seed.orEmpty()
|
||||
)
|
||||
}
|
||||
|
||||
"ws" -> {
|
||||
val wsSetting = streamSettings?.wsSettings ?: return null
|
||||
listOf("",
|
||||
listOf(
|
||||
"",
|
||||
wsSetting.headers.Host,
|
||||
wsSetting.path)
|
||||
wsSetting.path
|
||||
)
|
||||
}
|
||||
|
||||
"httpupgrade" -> {
|
||||
val httpupgradeSetting = streamSettings?.httpupgradeSettings ?: return null
|
||||
listOf("",
|
||||
listOf(
|
||||
"",
|
||||
httpupgradeSetting.host,
|
||||
httpupgradeSetting.path)
|
||||
httpupgradeSetting.path
|
||||
)
|
||||
}
|
||||
|
||||
"splithttp" -> {
|
||||
val splithttpSetting = streamSettings?.splithttpSettings ?: return null
|
||||
listOf(
|
||||
"",
|
||||
splithttpSetting.host,
|
||||
splithttpSetting.path
|
||||
)
|
||||
}
|
||||
|
||||
"h2" -> {
|
||||
val h2Setting = streamSettings?.httpSettings ?: return null
|
||||
listOf("",
|
||||
listOf(
|
||||
"",
|
||||
h2Setting.host.joinToString(),
|
||||
h2Setting.path)
|
||||
h2Setting.path
|
||||
)
|
||||
}
|
||||
|
||||
"quic" -> {
|
||||
val quicSetting = streamSettings?.quicSettings ?: return null
|
||||
listOf(quicSetting.header.type,
|
||||
listOf(
|
||||
quicSetting.header.type,
|
||||
quicSetting.security,
|
||||
quicSetting.key)
|
||||
quicSetting.key
|
||||
)
|
||||
}
|
||||
|
||||
"grpc" -> {
|
||||
val grpcSetting = streamSettings?.grpcSettings ?: return null
|
||||
listOf(if (grpcSetting.multiMode == true) "multi" else "gun",
|
||||
grpcSetting.authority ?: "",
|
||||
grpcSetting.serviceName)
|
||||
listOf(
|
||||
if (grpcSetting.multiMode == true) "multi" else "gun",
|
||||
grpcSetting.authority.orEmpty(),
|
||||
grpcSetting.serviceName
|
||||
)
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@@ -443,24 +561,29 @@ data class V2rayConfig(
|
||||
}
|
||||
}
|
||||
|
||||
data class DnsBean(var servers: ArrayList<Any>? = null,
|
||||
data class DnsBean(
|
||||
var servers: ArrayList<Any>? = null,
|
||||
var hosts: Map<String, Any>? = null,
|
||||
val clientIp: String? = null,
|
||||
val disableCache: Boolean? = null,
|
||||
val queryStrategy: String? = null,
|
||||
val tag: String? = null
|
||||
) {
|
||||
data class ServersBean(var address: String = "",
|
||||
data class ServersBean(
|
||||
var address: String = "",
|
||||
var port: Int? = null,
|
||||
var domains: List<String>? = null,
|
||||
var expectIPs: List<String>? = null,
|
||||
val clientIp: String? = null)
|
||||
val clientIp: String? = null
|
||||
)
|
||||
}
|
||||
|
||||
data class RoutingBean(var domainStrategy: String,
|
||||
data class RoutingBean(
|
||||
var domainStrategy: String,
|
||||
var domainMatcher: String? = null,
|
||||
var rules: ArrayList<RulesBean>,
|
||||
val balancers: List<Any>? = null) {
|
||||
val balancers: List<Any>? = null
|
||||
) {
|
||||
|
||||
data class RulesBean(
|
||||
var ip: ArrayList<String>? = null,
|
||||
@@ -479,8 +602,10 @@ data class V2rayConfig(
|
||||
)
|
||||
}
|
||||
|
||||
data class PolicyBean(var levels: Map<String, LevelBean>,
|
||||
var system: Any? = null) {
|
||||
data class PolicyBean(
|
||||
var levels: Map<String, LevelBean>,
|
||||
var system: Any? = null
|
||||
) {
|
||||
data class LevelBean(
|
||||
var handshake: Int? = null,
|
||||
var connIdle: Int? = null,
|
||||
@@ -488,11 +613,14 @@ data class V2rayConfig(
|
||||
var downlinkOnly: Int? = null,
|
||||
val statsUserUplink: Boolean? = null,
|
||||
val statsUserDownlink: Boolean? = null,
|
||||
var bufferSize: Int? = null)
|
||||
var bufferSize: Int? = null
|
||||
)
|
||||
}
|
||||
|
||||
data class FakednsBean(var ipPool: String = "198.18.0.0/15",
|
||||
var poolSize: Int = 10000) // roughly 10 times smaller than total ip pool
|
||||
data class FakednsBean(
|
||||
var ipPool: String = "198.18.0.0/15",
|
||||
var poolSize: Int = 10000
|
||||
) // roughly 10 times smaller than total ip pool
|
||||
|
||||
fun getProxyOutbound(): OutboundBean? {
|
||||
outbounds.forEach { outbound ->
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.v2ray.ang.dto
|
||||
|
||||
data class VmessQRCode(var v: String = "",
|
||||
data class VmessQRCode(
|
||||
var v: String = "",
|
||||
var ps: String = "",
|
||||
var add: String = "",
|
||||
var port: String = "",
|
||||
@@ -14,4 +15,5 @@ data class VmessQRCode(var v: String = "",
|
||||
var tls: String = "",
|
||||
var sni: String = "",
|
||||
var alpn: String = "",
|
||||
var fp: String = "")
|
||||
var fp: String = ""
|
||||
)
|
||||
@@ -9,15 +9,15 @@ import org.json.JSONObject
|
||||
import java.net.URI
|
||||
import java.net.URLConnection
|
||||
|
||||
val Context.v2RayApplication: AngApplication
|
||||
get() = applicationContext as AngApplication
|
||||
val Context.v2RayApplication: AngApplication?
|
||||
get() = applicationContext as? AngApplication
|
||||
|
||||
fun Context.toast(message: Int) {
|
||||
ToastCompat.makeText(this, message, Toast.LENGTH_SHORT).apply { show() }
|
||||
ToastCompat.makeText(this, message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
fun Context.toast(message: CharSequence) {
|
||||
ToastCompat.makeText(this, message, Toast.LENGTH_SHORT).apply { show() }
|
||||
ToastCompat.makeText(this, message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
fun JSONObject.putOpt(pair: Pair<String, Any?>) {
|
||||
@@ -34,26 +34,14 @@ const val DIVISOR = 1024.0
|
||||
fun Long.toSpeedString(): String = this.toTrafficString() + "/s"
|
||||
|
||||
fun Long.toTrafficString(): String {
|
||||
if (this < THRESHOLD) {
|
||||
return "$this B"
|
||||
val units = arrayOf("B", "KB", "MB", "GB", "TB", "PB")
|
||||
var size = this.toDouble()
|
||||
var unitIndex = 0
|
||||
while (size >= THRESHOLD && unitIndex < units.size - 1) {
|
||||
size /= DIVISOR
|
||||
unitIndex++
|
||||
}
|
||||
val kb = this / DIVISOR
|
||||
if (kb < THRESHOLD) {
|
||||
return "${String.format("%.1f KB", kb)}"
|
||||
}
|
||||
val mb = kb / DIVISOR
|
||||
if (mb < THRESHOLD) {
|
||||
return "${String.format("%.1f MB", mb)}"
|
||||
}
|
||||
val gb = mb / DIVISOR
|
||||
if (gb < THRESHOLD) {
|
||||
return "${String.format("%.1f GB", gb)}"
|
||||
}
|
||||
val tb = gb / DIVISOR
|
||||
if (tb < THRESHOLD) {
|
||||
return "${String.format("%.1f TB", tb)}"
|
||||
}
|
||||
return String.format("%.1f PB", tb / DIVISOR)
|
||||
return String.format("%.1f %s", size, units[unitIndex])
|
||||
}
|
||||
|
||||
val URLConnection.responseLength: Long
|
||||
@@ -64,6 +52,6 @@ val URLConnection.responseLength: Long
|
||||
}
|
||||
|
||||
val URI.idnHost: String
|
||||
get() = host?.replace("[", "")?.replace("]", "") ?: ""
|
||||
get() = host?.replace("[", "")?.replace("]", "").orEmpty()
|
||||
|
||||
fun String.removeWhiteSpace(): String = replace("\\s+".toRegex(), "")
|
||||
@@ -35,7 +35,8 @@ class WidgetProvider : AppWidgetProvider() {
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
} else {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
})
|
||||
}
|
||||
)
|
||||
remoteViews.setOnClickPendingIntent(R.id.layout_switch, pendingIntent)
|
||||
if (isRunning) {
|
||||
remoteViews.setInt(R.id.image_switch, "setImageResource", R.drawable.ic_stop_24dp)
|
||||
@@ -65,12 +66,17 @@ class WidgetProvider : AppWidgetProvider() {
|
||||
AppWidgetManager.getInstance(context)?.let { manager ->
|
||||
when (intent.getIntExtra("key", 0)) {
|
||||
AppConfig.MSG_STATE_RUNNING, AppConfig.MSG_STATE_START_SUCCESS -> {
|
||||
updateWidgetBackground(context, manager, manager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)),
|
||||
true)
|
||||
updateWidgetBackground(
|
||||
context, manager, manager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)),
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
AppConfig.MSG_STATE_NOT_RUNNING, AppConfig.MSG_STATE_START_FAILURE, AppConfig.MSG_STATE_STOP_SUCCESS -> {
|
||||
updateWidgetBackground(context, manager, manager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)),
|
||||
false)
|
||||
updateWidgetBackground(
|
||||
context, manager, manager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)),
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ class QSTileService : TileService() {
|
||||
Tile.STATE_INACTIVE -> {
|
||||
Utils.startVServiceFromToggle(this)
|
||||
}
|
||||
|
||||
Tile.STATE_ACTIVE -> {
|
||||
Utils.stopVService(this)
|
||||
}
|
||||
@@ -67,22 +68,26 @@ class QSTileService : TileService() {
|
||||
private var mMsgReceive: BroadcastReceiver? = null
|
||||
|
||||
private class ReceiveMessageHandler(context: QSTileService) : BroadcastReceiver() {
|
||||
internal var mReference: SoftReference<QSTileService> = SoftReference(context)
|
||||
var mReference: SoftReference<QSTileService> = SoftReference(context)
|
||||
override fun onReceive(ctx: Context?, intent: Intent?) {
|
||||
val context = mReference.get()
|
||||
when (intent?.getIntExtra("key", 0)) {
|
||||
AppConfig.MSG_STATE_RUNNING -> {
|
||||
context?.setState(Tile.STATE_ACTIVE)
|
||||
}
|
||||
|
||||
AppConfig.MSG_STATE_NOT_RUNNING -> {
|
||||
context?.setState(Tile.STATE_INACTIVE)
|
||||
}
|
||||
|
||||
AppConfig.MSG_STATE_START_SUCCESS -> {
|
||||
context?.setState(Tile.STATE_ACTIVE)
|
||||
}
|
||||
|
||||
AppConfig.MSG_STATE_START_FAILURE -> {
|
||||
context?.setState(Tile.STATE_INACTIVE)
|
||||
}
|
||||
|
||||
AppConfig.MSG_STATE_STOP_SUCCESS -> {
|
||||
context?.setState(Tile.STATE_INACTIVE)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.SUBSCRIPTION_UPDATE_CHANNEL
|
||||
import com.v2ray.ang.AppConfig.SUBSCRIPTION_UPDATE_CHANNEL_NAME
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.util.AngConfigManager
|
||||
import com.v2ray.ang.util.MmkvManager
|
||||
@@ -18,14 +20,13 @@ import com.v2ray.ang.util.Utils
|
||||
|
||||
object SubscriptionUpdater {
|
||||
|
||||
const val notificationChannel = "subscription_update_channel"
|
||||
|
||||
class UpdateTask(context: Context, params: WorkerParameters) :
|
||||
CoroutineWorker(context, params) {
|
||||
|
||||
private val notificationManager = NotificationManagerCompat.from(applicationContext)
|
||||
private val notification =
|
||||
NotificationCompat.Builder(applicationContext, notificationChannel)
|
||||
NotificationCompat.Builder(applicationContext, SUBSCRIPTION_UPDATE_CHANNEL)
|
||||
.setWhen(0)
|
||||
.setTicker("Update")
|
||||
.setContentTitle(context.getString(R.string.title_pref_auto_update_subscription))
|
||||
@@ -43,11 +44,11 @@ object SubscriptionUpdater {
|
||||
val subscription = i.second
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
notification.setChannelId(notificationChannel)
|
||||
notification.setChannelId(SUBSCRIPTION_UPDATE_CHANNEL)
|
||||
val channel =
|
||||
NotificationChannel(
|
||||
notificationChannel,
|
||||
"Subscription Update Service",
|
||||
SUBSCRIPTION_UPDATE_CHANNEL,
|
||||
SUBSCRIPTION_UPDATE_CHANNEL_NAME,
|
||||
NotificationManager.IMPORTANCE_MIN
|
||||
)
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
@@ -58,23 +59,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,7 @@ class V2RayProxyOnlyService : Service(), ServiceControl {
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
override fun attachBaseContext(newBase: Context?) {
|
||||
val context = newBase?.let {
|
||||
MyContextWrapper.wrap(newBase, Utils.getLocale(newBase))
|
||||
MyContextWrapper.wrap(newBase, Utils.getLocale())
|
||||
}
|
||||
super.attachBaseContext(context)
|
||||
}
|
||||
|
||||
@@ -27,14 +27,14 @@ import com.v2ray.ang.util.MmkvManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import com.v2ray.ang.util.V2rayConfigUtil
|
||||
import go.Seq
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import libv2ray.Libv2ray
|
||||
import libv2ray.V2RayPoint
|
||||
import libv2ray.V2RayVPNServiceSupportsSet
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import java.lang.ref.SoftReference
|
||||
import kotlin.math.min
|
||||
|
||||
@@ -59,16 +59,21 @@ object V2RayServiceManager {
|
||||
|
||||
private var lastQueryTime = 0L
|
||||
private var mBuilder: NotificationCompat.Builder? = null
|
||||
private var mSubscription: Subscription? = null
|
||||
private var mDisposable: Disposable? = null
|
||||
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,13 +170,12 @@ object V2RayServiceManager {
|
||||
cancelNotification()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stopV2rayPoint() {
|
||||
val service = serviceControl?.get()?.getService() ?: return
|
||||
|
||||
if (v2rayPoint.isRunning) {
|
||||
GlobalScope.launch(Dispatchers.Default) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
v2rayPoint.stopLoop()
|
||||
} catch (e: Exception) {
|
||||
@@ -200,18 +206,23 @@ object V2RayServiceManager {
|
||||
MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_NOT_RUNNING, "")
|
||||
}
|
||||
}
|
||||
|
||||
AppConfig.MSG_UNREGISTER_CLIENT -> {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
AppConfig.MSG_STATE_START -> {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
AppConfig.MSG_STATE_STOP -> {
|
||||
serviceControl.stopService()
|
||||
}
|
||||
|
||||
AppConfig.MSG_STATE_RESTART -> {
|
||||
startV2rayPoint()
|
||||
}
|
||||
|
||||
AppConfig.MSG_MEASURE_DELAY -> {
|
||||
measureV2rayDelay()
|
||||
}
|
||||
@@ -222,6 +233,7 @@ object V2RayServiceManager {
|
||||
Log.d(ANG_PACKAGE, "SCREEN_OFF, stop querying stats")
|
||||
stopSpeedNotification()
|
||||
}
|
||||
|
||||
Intent.ACTION_SCREEN_ON -> {
|
||||
Log.d(ANG_PACKAGE, "SCREEN_ON, start querying stats")
|
||||
startSpeedNotification()
|
||||
@@ -231,7 +243,7 @@ object V2RayServiceManager {
|
||||
}
|
||||
|
||||
private fun measureV2rayDelay() {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val service = serviceControl?.get()?.getService() ?: return@launch
|
||||
var time = -1L
|
||||
var errstr = ""
|
||||
@@ -242,6 +254,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)
|
||||
@@ -256,25 +276,29 @@ object V2RayServiceManager {
|
||||
private fun showNotification() {
|
||||
val service = serviceControl?.get()?.getService() ?: return
|
||||
val startMainIntent = Intent(service, MainActivity::class.java)
|
||||
val contentPendingIntent = PendingIntent.getActivity(service,
|
||||
val contentPendingIntent = PendingIntent.getActivity(
|
||||
service,
|
||||
NOTIFICATION_PENDING_INTENT_CONTENT, startMainIntent,
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
} else {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
val stopV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE)
|
||||
stopV2RayIntent.`package` = ANG_PACKAGE
|
||||
stopV2RayIntent.putExtra("key", AppConfig.MSG_STATE_STOP)
|
||||
|
||||
val stopV2RayPendingIntent = PendingIntent.getBroadcast(service,
|
||||
val stopV2RayPendingIntent = PendingIntent.getBroadcast(
|
||||
service,
|
||||
NOTIFICATION_PENDING_INTENT_STOP_V2RAY, stopV2RayIntent,
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
} else {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
val channelId =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
@@ -293,9 +317,11 @@ object V2RayServiceManager {
|
||||
.setShowWhen(false)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setContentIntent(contentPendingIntent)
|
||||
.addAction(R.drawable.ic_delete_24dp,
|
||||
.addAction(
|
||||
R.drawable.ic_delete_24dp,
|
||||
service.getString(R.string.notification_action_stop_v2ray),
|
||||
stopV2RayPendingIntent)
|
||||
stopV2RayPendingIntent
|
||||
)
|
||||
//.build()
|
||||
|
||||
//mBuilder?.setDefaults(NotificationCompat.FLAG_ONLY_ALERT_ONCE) //取消震动,铃声其他都不好使
|
||||
@@ -305,10 +331,12 @@ object V2RayServiceManager {
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private fun createNotificationChannel(): String {
|
||||
val channelId = "RAY_NG_M_CH_ID"
|
||||
val channelName = "V2rayNG Background Service"
|
||||
val chan = NotificationChannel(channelId,
|
||||
channelName, NotificationManager.IMPORTANCE_HIGH)
|
||||
val channelId = AppConfig.RAY_NG_CHANNEL_ID
|
||||
val channelName = AppConfig.RAY_NG_CHANNEL_NAME
|
||||
val chan = NotificationChannel(
|
||||
channelId,
|
||||
channelName, NotificationManager.IMPORTANCE_HIGH
|
||||
)
|
||||
chan.lightColor = Color.DKGRAY
|
||||
chan.importance = NotificationManager.IMPORTANCE_NONE
|
||||
chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
|
||||
@@ -320,8 +348,8 @@ object V2RayServiceManager {
|
||||
val service = serviceControl?.get()?.getService() ?: return
|
||||
service.stopForeground(true)
|
||||
mBuilder = null
|
||||
mSubscription?.unsubscribe()
|
||||
mSubscription = null
|
||||
mDisposable?.dispose()
|
||||
mDisposable = null
|
||||
}
|
||||
|
||||
private fun updateNotification(contentText: String?, proxyTraffic: Long, directTraffic: Long) {
|
||||
@@ -348,36 +376,39 @@ object V2RayServiceManager {
|
||||
}
|
||||
|
||||
private fun startSpeedNotification() {
|
||||
if (mSubscription == null &&
|
||||
if (mDisposable == null &&
|
||||
v2rayPoint.isRunning &&
|
||||
settingsStorage?.decodeBool(AppConfig.PREF_SPEED_ENABLED) == true) {
|
||||
settingsStorage?.decodeBool(AppConfig.PREF_SPEED_ENABLED) == true
|
||||
) {
|
||||
var lastZeroSpeed = false
|
||||
val outboundTags = currentConfig?.getAllOutboundTags()
|
||||
outboundTags?.remove(TAG_DIRECT)
|
||||
|
||||
mSubscription = Observable.interval(3, java.util.concurrent.TimeUnit.SECONDS)
|
||||
mDisposable = Observable.interval(3, java.util.concurrent.TimeUnit.SECONDS)
|
||||
.subscribe {
|
||||
val queryTime = System.currentTimeMillis()
|
||||
val sinceLastQueryInSeconds = (queryTime - lastQueryTime) / 1000.0
|
||||
var proxyTotal = 0L
|
||||
val text = StringBuilder()
|
||||
outboundTags?.forEach {
|
||||
val up = v2rayPoint.queryStats(it, "uplink")
|
||||
val down = v2rayPoint.queryStats(it, "downlink")
|
||||
val up = v2rayPoint.queryStats(it, AppConfig.UPLINK)
|
||||
val down = v2rayPoint.queryStats(it, AppConfig.DOWNLINK)
|
||||
if (up + down > 0) {
|
||||
appendSpeedString(text, it, up / sinceLastQueryInSeconds, down / sinceLastQueryInSeconds)
|
||||
proxyTotal += up + down
|
||||
}
|
||||
}
|
||||
val directUplink = v2rayPoint.queryStats(TAG_DIRECT, "uplink")
|
||||
val directDownlink = v2rayPoint.queryStats(TAG_DIRECT, "downlink")
|
||||
val directUplink = v2rayPoint.queryStats(TAG_DIRECT, AppConfig.UPLINK)
|
||||
val directDownlink = v2rayPoint.queryStats(TAG_DIRECT, AppConfig.DOWNLINK)
|
||||
val zeroSpeed = proxyTotal == 0L && directUplink == 0L && directDownlink == 0L
|
||||
if (!zeroSpeed || !lastZeroSpeed) {
|
||||
if (proxyTotal == 0L) {
|
||||
appendSpeedString(text, outboundTags?.firstOrNull(), 0.0, 0.0)
|
||||
}
|
||||
appendSpeedString(text, TAG_DIRECT, directUplink / sinceLastQueryInSeconds,
|
||||
directDownlink / sinceLastQueryInSeconds)
|
||||
appendSpeedString(
|
||||
text, TAG_DIRECT, directUplink / sinceLastQueryInSeconds,
|
||||
directDownlink / sinceLastQueryInSeconds
|
||||
)
|
||||
updateNotification(text.toString(), proxyTotal, directDownlink + directUplink)
|
||||
}
|
||||
lastZeroSpeed = zeroSpeed
|
||||
@@ -397,9 +428,9 @@ object V2RayServiceManager {
|
||||
}
|
||||
|
||||
private fun stopSpeedNotification() {
|
||||
if (mSubscription != null) {
|
||||
mSubscription?.unsubscribe() //stop queryStats
|
||||
mSubscription = null
|
||||
if (mDisposable != null) {
|
||||
mDisposable?.dispose() //stop queryStats
|
||||
mDisposable = null
|
||||
updateNotification(currentConfig?.remarks, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,11 @@ import com.v2ray.ang.util.MessageUtil
|
||||
import com.v2ray.ang.util.SpeedtestUtil
|
||||
import com.v2ray.ang.util.Utils
|
||||
import go.Seq
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.cancelChildren
|
||||
import kotlinx.coroutines.launch
|
||||
import libv2ray.Libv2ray
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
@@ -32,6 +36,7 @@ class V2RayTestService : Service() {
|
||||
MessageUtil.sendMsg2UI(this@V2RayTestService, MSG_MEASURE_CONFIG_SUCCESS, Pair(contentPair.first, result))
|
||||
}
|
||||
}
|
||||
|
||||
MSG_MEASURE_CONFIG_CANCEL -> {
|
||||
realTestScope.coroutineContext[Job]?.cancelChildren()
|
||||
}
|
||||
|
||||
@@ -4,7 +4,13 @@ import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.*
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.LocalSocket
|
||||
import android.net.LocalSocketAddress
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.os.StrictMode
|
||||
@@ -17,8 +23,8 @@ import com.v2ray.ang.dto.ERoutingMode
|
||||
import com.v2ray.ang.util.MmkvManager
|
||||
import com.v2ray.ang.util.MyContextWrapper
|
||||
import com.v2ray.ang.util.Utils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import java.lang.ref.SoftReference
|
||||
@@ -196,14 +202,16 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||
|
||||
private fun runTun2socks() {
|
||||
val socksPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt())
|
||||
val cmd = arrayListOf(File(applicationContext.applicationInfo.nativeLibraryDir, TUN2SOCKS).absolutePath,
|
||||
val cmd = arrayListOf(
|
||||
File(applicationContext.applicationInfo.nativeLibraryDir, TUN2SOCKS).absolutePath,
|
||||
"--netif-ipaddr", PRIVATE_VLAN4_ROUTER,
|
||||
"--netif-netmask", "255.255.255.252",
|
||||
"--socks-server-addr", "127.0.0.1:${socksPort}",
|
||||
"--tunmtu", VPN_MTU.toString(),
|
||||
"--sock-path", "sock_path",//File(applicationContext.filesDir, "sock_path").absolutePath,
|
||||
"--enable-udprelay",
|
||||
"--loglevel", "notice")
|
||||
"--loglevel", "notice"
|
||||
)
|
||||
|
||||
if (settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) == true) {
|
||||
cmd.add("--netif-ip6addr")
|
||||
@@ -244,7 +252,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||
val path = File(applicationContext.filesDir, "sock_path").absolutePath
|
||||
Log.d(packageName, path)
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
var tries = 0
|
||||
while (true) try {
|
||||
Thread.sleep(50L shl tries)
|
||||
@@ -274,7 +282,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||
// val emptyInfo = VpnNetworkInfo()
|
||||
// val info = loadVpnNetworkInfo(configName, emptyInfo)!! + (lastNetworkInfo ?: emptyInfo)
|
||||
// saveVpnNetworkInfo(configName, info)
|
||||
isRunning = false;
|
||||
isRunning = false
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
try {
|
||||
connectivity.unregisterNetworkCallback(defaultNetworkCallback)
|
||||
@@ -327,7 +335,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
override fun attachBaseContext(newBase: Context?) {
|
||||
val context = newBase?.let {
|
||||
MyContextWrapper.wrap(newBase, Utils.getLocale(newBase))
|
||||
MyContextWrapper.wrap(newBase, Utils.getLocale())
|
||||
}
|
||||
super.attachBaseContext(context)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.FileProvider
|
||||
import com.tbruyelle.rxpermissions.RxPermissions
|
||||
import com.tbruyelle.rxpermissions3.RxPermissions
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.BuildConfig
|
||||
@@ -21,14 +21,12 @@ import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class AboutActivity : BaseActivity() {
|
||||
private lateinit var binding: ActivityAboutBinding
|
||||
private val binding by lazy { ActivityAboutBinding.inflate(layoutInflater) }
|
||||
private val extDir by lazy { File(Utils.backupPath(this)) }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityAboutBinding.inflate(layoutInflater)
|
||||
val view = binding.root
|
||||
setContentView(view)
|
||||
setContentView(binding.root)
|
||||
|
||||
title = getString(R.string.title_about)
|
||||
|
||||
|
||||
@@ -23,16 +23,19 @@ abstract class BaseActivity : AppCompatActivity() {
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
onBackPressed()
|
||||
// Handles the home button press by delegating to the onBackPressedDispatcher.
|
||||
// This ensures consistent back navigation behavior.
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
override fun attachBaseContext(newBase: Context?) {
|
||||
val context = newBase?.let {
|
||||
MyContextWrapper.wrap(newBase, Utils.getLocale(newBase))
|
||||
MyContextWrapper.wrap(newBase, Utils.getLocale())
|
||||
}
|
||||
super.attachBaseContext(context)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.Bundle
|
||||
import android.text.method.ScrollingMovementMethod
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
@@ -14,20 +14,18 @@ import com.v2ray.ang.databinding.ActivityLogcatBinding
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.util.Utils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.IOException
|
||||
import java.util.LinkedHashSet
|
||||
|
||||
class LogcatActivity : BaseActivity() {
|
||||
private lateinit var binding: ActivityLogcatBinding
|
||||
private val binding by lazy {
|
||||
ActivityLogcatBinding.inflate(layoutInflater)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityLogcatBinding.inflate(layoutInflater)
|
||||
val view = binding.root
|
||||
setContentView(view)
|
||||
setContentView(binding.root)
|
||||
|
||||
title = getString(R.string.title_logcat)
|
||||
|
||||
@@ -44,9 +42,11 @@ class LogcatActivity : BaseActivity() {
|
||||
val lst = LinkedHashSet<String>()
|
||||
lst.add("logcat")
|
||||
lst.add("-c")
|
||||
withContext(Dispatchers.IO) {
|
||||
val process = Runtime.getRuntime().exec(lst.toTypedArray())
|
||||
process.waitFor()
|
||||
}
|
||||
}
|
||||
val lst = LinkedHashSet<String>()
|
||||
lst.add("logcat")
|
||||
lst.add("-d")
|
||||
@@ -54,7 +54,9 @@ class LogcatActivity : BaseActivity() {
|
||||
lst.add("time")
|
||||
lst.add("-s")
|
||||
lst.add("GoLog,tun2socks,${ANG_PACKAGE},AndroidRuntime,System.err")
|
||||
val process = Runtime.getRuntime().exec(lst.toTypedArray())
|
||||
val process = withContext(Dispatchers.IO) {
|
||||
Runtime.getRuntime().exec(lst.toTypedArray())
|
||||
}
|
||||
// val bufferedReader = BufferedReader(
|
||||
// InputStreamReader(process.inputStream))
|
||||
// val allText = bufferedReader.use(BufferedReader::readText)
|
||||
@@ -82,10 +84,12 @@ class LogcatActivity : BaseActivity() {
|
||||
toast(R.string.toast_success)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.clear_all -> {
|
||||
logcat(true)
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -19,16 +18,17 @@ import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.ActionBarDrawerToggle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.navigation.NavigationView
|
||||
import com.tbruyelle.rxpermissions.RxPermissions
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.tbruyelle.rxpermissions3.RxPermissions
|
||||
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.dto.EConfigType
|
||||
@@ -39,41 +39,55 @@ import com.v2ray.ang.util.AngConfigManager
|
||||
import com.v2ray.ang.util.MmkvManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import com.v2ray.ang.viewmodel.MainViewModel
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
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 {
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private val binding by lazy {
|
||||
ActivityMainBinding.inflate(layoutInflater)
|
||||
}
|
||||
|
||||
private val adapter by lazy { MainRecyclerAdapter(this) }
|
||||
private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
|
||||
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
|
||||
private val requestVpnPermission = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == RESULT_OK) {
|
||||
startV2Ray()
|
||||
}
|
||||
}
|
||||
private val requestSubSettingActivity = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
initGroupTab()
|
||||
}
|
||||
private val tabGroupListener = object : TabLayout.OnTabSelectedListener {
|
||||
override fun onTabSelected(tab: TabLayout.Tab?) {
|
||||
val selectId = tab?.tag.toString()
|
||||
if (selectId != mainViewModel.subscriptionId) {
|
||||
mainViewModel.subscriptionIdChanged(selectId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTabUnselected(tab: TabLayout.Tab?) {
|
||||
}
|
||||
|
||||
override fun onTabReselected(tab: TabLayout.Tab?) {
|
||||
}
|
||||
}
|
||||
private var mItemTouchHelper: ItemTouchHelper? = null
|
||||
val mainViewModel: MainViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
val view = binding.root
|
||||
setContentView(view)
|
||||
setContentView(binding.root)
|
||||
title = getString(R.string.title_server)
|
||||
setSupportActionBar(binding.toolbar)
|
||||
|
||||
binding.fab.setOnClickListener {
|
||||
if (mainViewModel.isRunning.value == true) {
|
||||
Utils.stopVService(this)
|
||||
} else if ((settingsStorage?.decodeString(AppConfig.PREF_MODE) ?: "VPN") == "VPN") {
|
||||
} else if ((MmkvManager.settingsStorage?.decodeString(AppConfig.PREF_MODE) ?: "VPN") == "VPN") {
|
||||
val intent = VpnService.prepare(this)
|
||||
if (intent == null) {
|
||||
startV2Ray()
|
||||
@@ -103,15 +117,14 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
|
||||
|
||||
val toggle = ActionBarDrawerToggle(
|
||||
this, binding.drawerLayout, binding.toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close)
|
||||
this, binding.drawerLayout, binding.toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close
|
||||
)
|
||||
binding.drawerLayout.addDrawerListener(toggle)
|
||||
toggle.syncState()
|
||||
binding.navView.setNavigationItemSelectedListener(this)
|
||||
|
||||
|
||||
initGroupTab()
|
||||
setupViewModel()
|
||||
copyAssets()
|
||||
//migrateLegacy()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
RxPermissions(this)
|
||||
@@ -158,49 +171,34 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
}
|
||||
}
|
||||
mainViewModel.startListenBroadcast()
|
||||
mainViewModel.copyAssets(assets)
|
||||
}
|
||||
|
||||
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 initGroupTab() {
|
||||
binding.tabGroup.removeOnTabSelectedListener(tabGroupListener)
|
||||
binding.tabGroup.removeAllTabs()
|
||||
binding.tabGroup.isVisible = false
|
||||
|
||||
val (listId, listRemarks) = mainViewModel.getSubscriptions(this)
|
||||
if (listId == null || listRemarks == null) {
|
||||
return
|
||||
}
|
||||
|
||||
// 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))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
for (it in listRemarks.indices) {
|
||||
val tab = binding.tabGroup.newTab()
|
||||
tab.text = listRemarks[it]
|
||||
tab.tag = listId[it]
|
||||
binding.tabGroup.addTab(tab)
|
||||
}
|
||||
val selectIndex =
|
||||
listId.indexOf(mainViewModel.subscriptionId).takeIf { it >= 0 } ?: (listId.count() - 1)
|
||||
binding.tabGroup.selectTab(binding.tabGroup.getTabAt(selectIndex))
|
||||
binding.tabGroup.addOnTabSelectedListener(tabGroupListener)
|
||||
binding.tabGroup.isVisible = true
|
||||
}
|
||||
|
||||
fun startV2Ray() {
|
||||
if (mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER).isNullOrEmpty()) {
|
||||
if (MmkvManager.mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER).isNullOrEmpty()) {
|
||||
return
|
||||
}
|
||||
V2RayServiceManager.startV2Ray(this)
|
||||
@@ -228,7 +226,25 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.menu_main, menu)
|
||||
return true
|
||||
|
||||
val searchItem = menu.findItem(R.id.search_view)
|
||||
if (searchItem != null) {
|
||||
val searchView = searchItem.actionView as SearchView
|
||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String?): Boolean = false
|
||||
|
||||
override fun onQueryTextChange(newText: String?): Boolean {
|
||||
mainViewModel.filterConfig(newText.orEmpty())
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
searchView.setOnCloseListener {
|
||||
mainViewModel.filterConfig("")
|
||||
false
|
||||
}
|
||||
}
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||
@@ -236,67 +252,80 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
importQRcode(true)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.import_clipboard -> {
|
||||
importClipboard()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.import_manually_vmess -> {
|
||||
importManually(EConfigType.VMESS.value)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.import_manually_vless -> {
|
||||
importManually(EConfigType.VLESS.value)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.import_manually_ss -> {
|
||||
importManually(EConfigType.SHADOWSOCKS.value)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.import_manually_socks -> {
|
||||
importManually(EConfigType.SOCKS.value)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.import_manually_trojan -> {
|
||||
importManually(EConfigType.TROJAN.value)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.import_manually_wireguard -> {
|
||||
importManually(EConfigType.WIREGUARD.value)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.import_config_custom_clipboard -> {
|
||||
importConfigCustomClipboard()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.import_config_custom_local -> {
|
||||
importConfigCustomLocal()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.import_config_custom_url -> {
|
||||
importConfigCustomUrlClipboard()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.import_config_custom_url_scan -> {
|
||||
importQRcode(false)
|
||||
true
|
||||
}
|
||||
|
||||
// R.id.sub_setting -> {
|
||||
// startActivity<SubSettingActivity>()
|
||||
// true
|
||||
// }
|
||||
|
||||
R.id.sub_update -> {
|
||||
importConfigViaSub()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.export_all -> {
|
||||
if (AngConfigManager.shareNonCustomConfigsToClipboard(this, mainViewModel.serverList) == 0) {
|
||||
binding.pbWaiting.show()
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val ret = mainViewModel.exportAllServer()
|
||||
launch(Dispatchers.Main) {
|
||||
if (ret == 0)
|
||||
toast(R.string.toast_success)
|
||||
} else {
|
||||
else
|
||||
toast(R.string.toast_failure)
|
||||
binding.pbWaiting.hide()
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
@@ -318,8 +347,14 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
R.id.del_all_config -> {
|
||||
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
MmkvManager.removeAllServer()
|
||||
binding.pbWaiting.show()
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
mainViewModel.removeAllServer()
|
||||
launch(Dispatchers.Main) {
|
||||
mainViewModel.reloadServerList()
|
||||
binding.pbWaiting.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.no) { _, _ ->
|
||||
//do noting
|
||||
@@ -327,10 +362,19 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
.show()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.del_duplicate_config -> {
|
||||
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
mainViewModel.removeDuplicateServer()
|
||||
binding.pbWaiting.show()
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val ret = mainViewModel.removeDuplicateServer()
|
||||
launch(Dispatchers.Main) {
|
||||
mainViewModel.reloadServerList()
|
||||
toast(getString(R.string.title_del_duplicate_config_count, ret))
|
||||
binding.pbWaiting.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.no) { _, _ ->
|
||||
//do noting
|
||||
@@ -338,11 +382,18 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
.show()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.del_invalid_config -> {
|
||||
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
||||
AlertDialog.Builder(this).setMessage(R.string.del_invalid_config_comfirm)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
MmkvManager.removeInvalidServer()
|
||||
binding.pbWaiting.show()
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
mainViewModel.removeInvalidServer()
|
||||
launch(Dispatchers.Main) {
|
||||
mainViewModel.reloadServerList()
|
||||
binding.pbWaiting.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.no) { _, _ ->
|
||||
//do noting
|
||||
@@ -350,13 +401,16 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
.show()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.sort_by_test_results -> {
|
||||
MmkvManager.sortByTestResults()
|
||||
binding.pbWaiting.show()
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
mainViewModel.sortByTestResults()
|
||||
launch(Dispatchers.Main) {
|
||||
mainViewModel.reloadServerList()
|
||||
true
|
||||
binding.pbWaiting.hide()
|
||||
}
|
||||
}
|
||||
R.id.filter_config -> {
|
||||
mainViewModel.filterConfig(this)
|
||||
true
|
||||
}
|
||||
|
||||
@@ -375,7 +429,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 +465,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
/**
|
||||
* import config from clipboard
|
||||
*/
|
||||
fun importClipboard()
|
||||
private fun importClipboard()
|
||||
: Boolean {
|
||||
try {
|
||||
val clipboard = Utils.getClipboard(this)
|
||||
@@ -423,30 +477,32 @@ 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()
|
||||
binding.pbWaiting.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, countSub) = AngConfigManager.importBatchConfig(server, mainViewModel.subscriptionId, true)
|
||||
delay(500L)
|
||||
launch(Dispatchers.Main) {
|
||||
if (count > 0) {
|
||||
toast(R.string.toast_success)
|
||||
mainViewModel.reloadServerList()
|
||||
} else if (countSub > 0) {
|
||||
initGroupTab()
|
||||
} else {
|
||||
toast(R.string.toast_failure)
|
||||
}
|
||||
//dialog.dismiss()
|
||||
binding.pbWaiting.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun importConfigCustomClipboard()
|
||||
private fun importConfigCustomClipboard()
|
||||
: Boolean {
|
||||
try {
|
||||
val configText = Utils.getClipboard(this)
|
||||
@@ -465,7 +521,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 +531,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
return true
|
||||
}
|
||||
|
||||
fun importConfigCustomUrlClipboard()
|
||||
private fun importConfigCustomUrlClipboard()
|
||||
: Boolean {
|
||||
try {
|
||||
val url = Utils.getClipboard(this)
|
||||
@@ -493,7 +549,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 +576,26 @@ 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()
|
||||
binding.pbWaiting.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 = mainViewModel.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()
|
||||
binding.pbWaiting.hide()
|
||||
}
|
||||
launch(Dispatchers.Main) {
|
||||
importBatchConfig(configText, it.first)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -623,15 +650,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 +670,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
}
|
||||
}
|
||||
|
||||
fun setTestState(content: String?) {
|
||||
private fun setTestState(content: String?) {
|
||||
binding.tvTestState.text = content
|
||||
}
|
||||
|
||||
@@ -661,26 +691,33 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
return super.onKeyDown(keyCode, event)
|
||||
}
|
||||
|
||||
|
||||
override fun onNavigationItemSelected(item: MenuItem): Boolean {
|
||||
// Handle navigation view item clicks here.
|
||||
when (item.itemId) {
|
||||
//R.id.server_profile -> activityClass = MainActivity::class.java
|
||||
R.id.sub_setting -> {
|
||||
startActivity(Intent(this, SubSettingActivity::class.java))
|
||||
requestSubSettingActivity.launch(Intent(this, SubSettingActivity::class.java))
|
||||
}
|
||||
|
||||
R.id.settings -> {
|
||||
startActivity(Intent(this, SettingsActivity::class.java)
|
||||
.putExtra("isRunning", mainViewModel.isRunning.value == true))
|
||||
startActivity(
|
||||
Intent(this, SettingsActivity::class.java)
|
||||
.putExtra("isRunning", mainViewModel.isRunning.value == true)
|
||||
)
|
||||
}
|
||||
|
||||
R.id.user_asset_setting -> {
|
||||
startActivity(Intent(this, UserAssetActivity::class.java))
|
||||
}
|
||||
|
||||
R.id.promotion -> {
|
||||
Utils.openUri(this, "${Utils.decode(AppConfig.PromotionUrl)}?t=${System.currentTimeMillis()}")
|
||||
}
|
||||
|
||||
R.id.logcat -> {
|
||||
startActivity(Intent(this, LogcatActivity::class.java))
|
||||
}
|
||||
|
||||
R.id.about -> {
|
||||
startActivity(Intent(this, AboutActivity::class.java))
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.gson.Gson
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AngApplication.Companion.application
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
@@ -26,21 +25,17 @@ import com.v2ray.ang.service.V2RayServiceManager
|
||||
import com.v2ray.ang.util.AngConfigManager
|
||||
import com.v2ray.ang.util.MmkvManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<MainRecyclerAdapter.BaseViewHolder>()
|
||||
, ItemTouchHelperAdapter {
|
||||
class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<MainRecyclerAdapter.BaseViewHolder>(), ItemTouchHelperAdapter {
|
||||
companion object {
|
||||
private const val VIEW_TYPE_ITEM = 1
|
||||
private const val VIEW_TYPE_FOOTER = 2
|
||||
}
|
||||
|
||||
private var mActivity: MainActivity = activity
|
||||
private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
|
||||
private val subStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SUB, MMKV.MULTI_PROCESS_MODE) }
|
||||
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
|
||||
private val share_method: Array<out String> by lazy {
|
||||
mActivity.resources.getStringArray(R.array.share_method)
|
||||
}
|
||||
@@ -51,7 +46,7 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
||||
override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
|
||||
if (holder is MainViewHolder) {
|
||||
val guid = mActivity.mainViewModel.serversCache[position].guid
|
||||
val config = mActivity.mainViewModel.serversCache[position].config
|
||||
val profile = mActivity.mainViewModel.serversCache[position].profile
|
||||
// //filter
|
||||
// if (mActivity.mainViewModel.subscriptionId.isNotEmpty()
|
||||
// && mActivity.mainViewModel.subscriptionId != config.subscriptionId
|
||||
@@ -61,43 +56,46 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
||||
// holder.itemMainBinding.cardView.visibility = View.VISIBLE
|
||||
// }
|
||||
|
||||
val outbound = config.getProxyOutbound()
|
||||
val aff = MmkvManager.decodeServerAffiliationInfo(guid)
|
||||
|
||||
holder.itemMainBinding.tvName.text = config.remarks
|
||||
holder.itemMainBinding.tvName.text = profile.remarks
|
||||
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
||||
holder.itemMainBinding.tvTestResult.text = aff?.getTestDelayString() ?: ""
|
||||
holder.itemMainBinding.tvTestResult.text = aff?.getTestDelayString().orEmpty()
|
||||
if ((aff?.testDelayMillis ?: 0L) < 0L) {
|
||||
holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPingRed))
|
||||
} else {
|
||||
holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPing))
|
||||
}
|
||||
if (guid == mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) {
|
||||
if (guid == MmkvManager.mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) {
|
||||
holder.itemMainBinding.layoutIndicator.setBackgroundResource(R.color.colorAccent)
|
||||
} else {
|
||||
holder.itemMainBinding.layoutIndicator.setBackgroundResource(0)
|
||||
}
|
||||
holder.itemMainBinding.tvSubscription.text = ""
|
||||
val json = subStorage?.decodeString(config.subscriptionId)
|
||||
val json = MmkvManager.subStorage?.decodeString(profile.subscriptionId)
|
||||
if (!json.isNullOrBlank()) {
|
||||
val sub = Gson().fromJson(json, SubscriptionItem::class.java)
|
||||
holder.itemMainBinding.tvSubscription.text = sub.remarks
|
||||
}
|
||||
|
||||
var shareOptions = share_method.asList()
|
||||
when (config.configType) {
|
||||
when (profile.configType) {
|
||||
EConfigType.CUSTOM -> {
|
||||
holder.itemMainBinding.tvType.text = mActivity.getString(R.string.server_customize_config)
|
||||
shareOptions = shareOptions.takeLast(1)
|
||||
}
|
||||
|
||||
EConfigType.VLESS -> {
|
||||
holder.itemMainBinding.tvType.text = config.configType.name
|
||||
holder.itemMainBinding.tvType.text = profile.configType.name
|
||||
}
|
||||
|
||||
else -> {
|
||||
holder.itemMainBinding.tvType.text = config.configType.name.lowercase()
|
||||
holder.itemMainBinding.tvType.text = profile.configType.name.lowercase()
|
||||
}
|
||||
}
|
||||
val strState = "${outbound?.getServerAddress()?.dropLast(3)}*** : ${outbound?.getServerPort()}"
|
||||
|
||||
val strState = "${profile.server?.dropLast(3)}*** : ${profile.serverPort}"
|
||||
|
||||
holder.itemMainBinding.tvStatistics.text = strState
|
||||
|
||||
holder.itemMainBinding.layoutShare.setOnClickListener {
|
||||
@@ -105,7 +103,7 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
||||
try {
|
||||
when (i) {
|
||||
0 -> {
|
||||
if (config.configType == EConfigType.CUSTOM) {
|
||||
if (profile.configType == EConfigType.CUSTOM) {
|
||||
shareFullContent(guid)
|
||||
} else {
|
||||
val ivBinding = ItemQrcodeBinding.inflate(LayoutInflater.from(mActivity))
|
||||
@@ -113,6 +111,7 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
||||
AlertDialog.Builder(mActivity).setView(ivBinding.root).show()
|
||||
}
|
||||
}
|
||||
|
||||
1 -> {
|
||||
if (AngConfigManager.share2Clipboard(mActivity, guid) == 0) {
|
||||
mActivity.toast(R.string.toast_success)
|
||||
@@ -120,6 +119,7 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
||||
mActivity.toast(R.string.toast_failure)
|
||||
}
|
||||
}
|
||||
|
||||
2 -> shareFullContent(guid)
|
||||
else -> mActivity.toast("else")
|
||||
}
|
||||
@@ -132,15 +132,15 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
||||
holder.itemMainBinding.layoutEdit.setOnClickListener {
|
||||
val intent = Intent().putExtra("guid", guid)
|
||||
.putExtra("isRunning", isRunning)
|
||||
if (config.configType == EConfigType.CUSTOM) {
|
||||
if (profile.configType == EConfigType.CUSTOM) {
|
||||
mActivity.startActivity(intent.setClass(mActivity, ServerCustomConfigActivity::class.java))
|
||||
} else {
|
||||
mActivity.startActivity(intent.setClass(mActivity, ServerActivity::class.java))
|
||||
}
|
||||
}
|
||||
holder.itemMainBinding.layoutRemove.setOnClickListener {
|
||||
if (guid != mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) {
|
||||
if (settingsStorage?.decodeBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
|
||||
if (guid != MmkvManager.mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) {
|
||||
if (MmkvManager.settingsStorage?.decodeBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
|
||||
AlertDialog.Builder(mActivity).setMessage(R.string.del_config_comfirm)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
removeServer(guid, position)
|
||||
@@ -158,11 +158,11 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
||||
}
|
||||
|
||||
holder.itemMainBinding.infoContainer.setOnClickListener {
|
||||
val selected = mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)
|
||||
val selected = MmkvManager.mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)
|
||||
if (guid != selected) {
|
||||
mainStorage?.encode(MmkvManager.KEY_SELECTED_SERVER, guid)
|
||||
MmkvManager.mainStorage?.encode(MmkvManager.KEY_SELECTED_SERVER, guid)
|
||||
if (!TextUtils.isEmpty(selected)) {
|
||||
notifyItemChanged(mActivity.mainViewModel.getPosition(selected!!))
|
||||
notifyItemChanged(mActivity.mainViewModel.getPosition(selected.orEmpty()))
|
||||
}
|
||||
notifyItemChanged(mActivity.mainViewModel.getPosition(guid))
|
||||
if (isRunning) {
|
||||
@@ -206,6 +206,7 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
||||
return when (viewType) {
|
||||
VIEW_TYPE_ITEM ->
|
||||
MainViewHolder(ItemRecyclerMainBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||
|
||||
else ->
|
||||
FooterViewHolder(ItemRecyclerFooterBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||
}
|
||||
@@ -237,7 +238,7 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
||||
|
||||
override fun onItemDismiss(position: Int) {
|
||||
val guid = mActivity.mainViewModel.serversCache.getOrNull(position)?.guid ?: return
|
||||
if (guid != mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) {
|
||||
if (guid != MmkvManager.mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) {
|
||||
// mActivity.alert(R.string.del_config_comfirm) {
|
||||
// positiveButton(android.R.string.ok) {
|
||||
mActivity.mainViewModel.removeServer(guid)
|
||||
|
||||
@@ -21,14 +21,16 @@ import com.v2ray.ang.extension.v2RayApplication
|
||||
import com.v2ray.ang.util.AppManagerUtil
|
||||
import com.v2ray.ang.util.MmkvManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import java.text.Collator
|
||||
|
||||
class PerAppProxyActivity : BaseActivity() {
|
||||
private lateinit var binding: ActivityBypassListBinding
|
||||
private val binding by lazy {
|
||||
ActivityBypassListBinding.inflate(layoutInflater)
|
||||
}
|
||||
|
||||
private var adapter: PerAppProxyAdapter? = null
|
||||
private var appsAll: List<AppInfo>? = null
|
||||
@@ -36,9 +38,7 @@ class PerAppProxyActivity : BaseActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityBypassListBinding.inflate(layoutInflater)
|
||||
val view = binding.root
|
||||
setContentView(view)
|
||||
setContentView(binding.root)
|
||||
|
||||
val dividerItemDecoration = DividerItemDecoration(this, LinearLayoutManager.VERTICAL)
|
||||
binding.recyclerView.addItemDecoration(dividerItemDecoration)
|
||||
@@ -188,12 +188,10 @@ class PerAppProxyActivity : BaseActivity() {
|
||||
if (searchItem != null) {
|
||||
val searchView = searchItem.actionView as SearchView
|
||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||
return false
|
||||
}
|
||||
override fun onQueryTextSubmit(query: String?): Boolean = false
|
||||
|
||||
override fun onQueryTextChange(newText: String?): Boolean {
|
||||
filterProxyApp(newText!!)
|
||||
filterProxyApp(newText.orEmpty())
|
||||
return false
|
||||
}
|
||||
})
|
||||
@@ -209,29 +207,33 @@ 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()
|
||||
true
|
||||
} ?: false
|
||||
|
||||
R.id.select_proxy_app -> {
|
||||
selectProxyApp()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.import_proxy_app -> {
|
||||
importProxyApp()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.export_proxy_app -> {
|
||||
exportProxyApp()
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
@@ -250,9 +252,7 @@ class PerAppProxyActivity : BaseActivity() {
|
||||
|
||||
private fun importProxyApp() {
|
||||
val content = Utils.getClipboard(applicationContext)
|
||||
if (TextUtils.isEmpty(content)) {
|
||||
return
|
||||
}
|
||||
if (TextUtils.isEmpty(content)) return
|
||||
selectProxyApp(content, false)
|
||||
toast(R.string.toast_success)
|
||||
}
|
||||
@@ -274,11 +274,9 @@ class PerAppProxyActivity : BaseActivity() {
|
||||
} else {
|
||||
content
|
||||
}
|
||||
if (TextUtils.isEmpty(proxyApps)) {
|
||||
return false
|
||||
}
|
||||
if (TextUtils.isEmpty(proxyApps)) return false
|
||||
|
||||
adapter?.blacklist!!.clear()
|
||||
adapter?.blacklist?.clear()
|
||||
|
||||
if (binding.switchBypassApps.isChecked) {
|
||||
adapter?.let {
|
||||
@@ -286,7 +284,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 +297,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
|
||||
}
|
||||
@@ -316,12 +314,8 @@ class PerAppProxyActivity : BaseActivity() {
|
||||
|
||||
private fun inProxyApps(proxyApps: String, packageName: String, force: Boolean): Boolean {
|
||||
if (force) {
|
||||
if (packageName == "com.google.android.webview") {
|
||||
return false
|
||||
}
|
||||
if (packageName.startsWith("com.google")) {
|
||||
return true
|
||||
}
|
||||
if (packageName == "com.google.android.webview") return false
|
||||
if (packageName.startsWith("com.google")) return true
|
||||
}
|
||||
|
||||
return proxyApps.indexOf(packageName) >= 0
|
||||
@@ -334,7 +328,8 @@ class PerAppProxyActivity : BaseActivity() {
|
||||
if (key.isNotEmpty()) {
|
||||
appsAll?.forEach {
|
||||
if (it.appName.uppercase().indexOf(key) >= 0
|
||||
|| it.packageName.uppercase().indexOf(key) >= 0) {
|
||||
|| it.packageName.uppercase().indexOf(key) >= 0
|
||||
) {
|
||||
apps.add(it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ItemRecyclerBypassListBinding
|
||||
import com.v2ray.ang.dto.AppInfo
|
||||
import java.util.*
|
||||
|
||||
class PerAppProxyAdapter(val activity: BaseActivity, val apps: List<AppInfo>, blacklist: MutableSet<String>?) :
|
||||
RecyclerView.Adapter<PerAppProxyAdapter.BaseViewHolder>() {
|
||||
@@ -34,8 +33,10 @@ class PerAppProxyAdapter(val activity: BaseActivity, val apps: List<AppInfo>, bl
|
||||
return when (viewType) {
|
||||
VIEW_TYPE_HEADER -> {
|
||||
val view = View(ctx)
|
||||
view.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ctx.resources.getDimensionPixelSize(R.dimen.bypass_list_header_height) * 0)
|
||||
view.layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ctx.resources.getDimensionPixelSize(R.dimen.bypass_list_header_height) * 0
|
||||
)
|
||||
BaseViewHolder(view)
|
||||
}
|
||||
// VIEW_TYPE_ITEM -> AppViewHolder(ctx.layoutInflater
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import com.v2ray.ang.R
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ActivityRoutingSettingsBinding
|
||||
|
||||
class RoutingSettingsActivity : BaseActivity() {
|
||||
private lateinit var binding: ActivityRoutingSettingsBinding
|
||||
private val binding by lazy { ActivityRoutingSettingsBinding.inflate(layoutInflater) }
|
||||
|
||||
private val titles: Array<out String> by lazy {
|
||||
resources.getStringArray(R.array.routing_tag)
|
||||
@@ -16,9 +16,7 @@ class RoutingSettingsActivity : BaseActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityRoutingSettingsBinding.inflate(layoutInflater)
|
||||
val view = binding.root
|
||||
setContentView(view)
|
||||
setContentView(binding.root)
|
||||
|
||||
title = getString(R.string.title_pref_routing_custom)
|
||||
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.Manifest
|
||||
import androidx.appcompat.app.AppCompatActivity.RESULT_OK
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.view.*
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity.RESULT_OK
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.tbruyelle.rxpermissions.RxPermissions
|
||||
import com.tbruyelle.rxpermissions3.RxPermissions
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
@@ -23,17 +27,19 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class RoutingSettingsFragment : Fragment() {
|
||||
private lateinit var binding: FragmentRoutingSettingsBinding
|
||||
private val binding by lazy { FragmentRoutingSettingsBinding.inflate(layoutInflater) }
|
||||
|
||||
companion object {
|
||||
private const val routing_arg = "routing_arg"
|
||||
}
|
||||
|
||||
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View? {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
// Inflate the layout for this fragment
|
||||
binding = FragmentRoutingSettingsBinding.inflate(layoutInflater)
|
||||
return binding.root// inflater.inflate(R.layout.fragment_routing_settings, container, false)
|
||||
}
|
||||
|
||||
@@ -49,7 +55,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)
|
||||
}
|
||||
@@ -64,22 +70,27 @@ class RoutingSettingsFragment : Fragment() {
|
||||
saveRouting()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.del_routing -> {
|
||||
binding.etRoutingContent.text = null
|
||||
true
|
||||
}
|
||||
|
||||
R.id.scan_replace -> {
|
||||
scanQRcode(true)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.scan_append -> {
|
||||
scanQRcode(false)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.default_rules -> {
|
||||
setDefaultRules()
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
@@ -113,7 +124,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,9 +142,11 @@ class RoutingSettingsFragment : Fragment() {
|
||||
AppConfig.PREF_V2RAY_ROUTING_AGENT -> {
|
||||
tag = AppConfig.TAG_PROXY
|
||||
}
|
||||
|
||||
AppConfig.PREF_V2RAY_ROUTING_DIRECT -> {
|
||||
tag = AppConfig.TAG_DIRECT
|
||||
}
|
||||
|
||||
AppConfig.PREF_V2RAY_ROUTING_BLOCKED -> {
|
||||
tag = AppConfig.TAG_BLOCKED
|
||||
}
|
||||
@@ -145,7 +158,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
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.content.*
|
||||
import com.tbruyelle.rxpermissions.RxPermissions
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.util.AngConfigManager
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import com.tbruyelle.rxpermissions3.RxPermissions
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.util.AngConfigManager
|
||||
|
||||
class ScScannerActivity : BaseActivity() {
|
||||
|
||||
@@ -32,8 +32,8 @@ class ScScannerActivity : BaseActivity() {
|
||||
|
||||
private val scanQRCode = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == RESULT_OK) {
|
||||
val count = AngConfigManager.importBatchConfig(it.data?.getStringExtra("SCAN_RESULT"), "", false)
|
||||
if (count > 0) {
|
||||
val (count, countSub) = AngConfigManager.importBatchConfig(it.data?.getStringExtra("SCAN_RESULT"), "", false)
|
||||
if (count + countSub > 0) {
|
||||
toast(R.string.toast_success)
|
||||
} else {
|
||||
toast(R.string.toast_failure)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.util.Utils
|
||||
import android.os.Bundle
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.service.V2RayServiceManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
|
||||
class ScSwitchActivity : BaseActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.Manifest
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.os.Bundle
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import com.tbruyelle.rxpermissions.RxPermissions
|
||||
import com.tbruyelle.rxpermissions3.RxPermissions
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
@@ -45,7 +44,7 @@ class ScannerActivity : BaseActivity(){
|
||||
|
||||
private fun handleResult(result: QRResult) {
|
||||
if (result is QRResult.QRSuccess) {
|
||||
finished(result.content.rawValue!!)
|
||||
finished(result.content.rawValue.orEmpty())
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
@@ -54,7 +53,7 @@ class ScannerActivity : BaseActivity(){
|
||||
private fun finished(text: String) {
|
||||
val intent = Intent()
|
||||
intent.putExtra("SCAN_RESULT", text)
|
||||
setResult(AppCompatActivity.RESULT_OK, intent)
|
||||
setResult(RESULT_OK, intent)
|
||||
finish()
|
||||
}
|
||||
|
||||
@@ -68,6 +67,7 @@ class ScannerActivity : BaseActivity(){
|
||||
launchScan()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.select_photo -> {
|
||||
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
Manifest.permission.READ_MEDIA_IMAGES
|
||||
@@ -88,6 +88,7 @@ class ScannerActivity : BaseActivity(){
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
@@ -110,7 +111,7 @@ class ScannerActivity : BaseActivity(){
|
||||
try {
|
||||
val bitmap = BitmapFactory.decodeStream(contentResolver.openInputStream(uri))
|
||||
val text = QRCodeDecoder.syncDecodeQRCode(bitmap)
|
||||
finished(text!!)
|
||||
finished(text.orEmpty())
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
toast(e.message.toString())
|
||||
|
||||
@@ -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()).orEmpty()
|
||||
)
|
||||
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.orEmpty()
|
||||
}
|
||||
|
||||
MmkvManager.encodeServerConfig(editGuid, config)
|
||||
|
||||
@@ -8,7 +8,7 @@ import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.blacksquircle.ui.editorkit.utils.EditorTheme
|
||||
import com.blacksquircle.ui.language.json.JsonLanguage
|
||||
import com.google.gson.*
|
||||
import com.google.gson.Gson
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ActivityServerCustomConfigBinding
|
||||
@@ -21,7 +21,7 @@ import com.v2ray.ang.util.Utils
|
||||
import me.drakeet.support.toast.ToastCompat
|
||||
|
||||
class ServerCustomConfigActivity : BaseActivity() {
|
||||
private lateinit var binding: ActivityServerCustomConfigBinding
|
||||
private val binding by lazy { ActivityServerCustomConfigBinding.inflate(layoutInflater) }
|
||||
|
||||
private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
|
||||
private val serverRawStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SERVER_RAW, MMKV.MULTI_PROCESS_MODE) }
|
||||
@@ -34,9 +34,7 @@ class ServerCustomConfigActivity : BaseActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityServerCustomConfigBinding.inflate(layoutInflater)
|
||||
val view = binding.root
|
||||
setContentView(view)
|
||||
setContentView(binding.root)
|
||||
title = getString(R.string.title_server)
|
||||
|
||||
if (!Utils.getDarkModeStatus(this)) {
|
||||
@@ -91,7 +89,7 @@ class ServerCustomConfigActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
val config = MmkvManager.decodeServerConfig(editGuid) ?: ServerConfig.create(EConfigType.CUSTOM)
|
||||
config.remarks = v2rayConfig.remarks ?: binding.etRemarks.text.toString().trim()
|
||||
config.remarks = if (binding.etRemarks.text.isNullOrEmpty()) v2rayConfig.remarks.orEmpty() else binding.etRemarks.text.toString()
|
||||
config.fullConfig = v2rayConfig
|
||||
|
||||
MmkvManager.encodeServerConfig(editGuid, config)
|
||||
@@ -141,10 +139,12 @@ class ServerCustomConfigActivity : BaseActivity() {
|
||||
deleteServer()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.save_config -> {
|
||||
saveServer()
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,7 +198,8 @@ class SettingsActivity : BaseActivity() {
|
||||
fragmentInterval?.summary = settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20")
|
||||
|
||||
autoUpdateCheck?.isChecked = settingsStorage.getBoolean(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
|
||||
autoUpdateInterval?.summary = settingsStorage.decodeString(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL,AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL)
|
||||
autoUpdateInterval?.summary =
|
||||
settingsStorage.decodeString(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL, AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL)
|
||||
autoUpdateInterval?.isEnabled = settingsStorage.getBoolean(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
|
||||
|
||||
socksPort?.summary = settingsStorage.decodeString(AppConfig.PREF_SOCKS_PORT, AppConfig.PORT_SOCKS)
|
||||
@@ -350,12 +351,15 @@ class SettingsActivity : BaseActivity() {
|
||||
updateFragmentInterval(settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFragmentPackets(value: String?) {
|
||||
fragmentPackets?.summary = value.toString()
|
||||
}
|
||||
|
||||
private fun updateFragmentLength(value: String?) {
|
||||
fragmentLength?.summary = value.toString()
|
||||
}
|
||||
|
||||
private fun updateFragmentInterval(value: String?) {
|
||||
fragmentInterval?.summary = value.toString()
|
||||
}
|
||||
|
||||
@@ -5,25 +5,20 @@ import android.text.TextUtils
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.NetworkType
|
||||
import androidx.work.PeriodicWorkRequest
|
||||
import androidx.work.multiprocess.RemoteWorkManager
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.gson.Gson
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AngApplication
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ActivitySubEditBinding
|
||||
import com.v2ray.ang.dto.SubscriptionItem
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.service.SubscriptionUpdater
|
||||
import com.v2ray.ang.util.MmkvManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class SubEditActivity : BaseActivity() {
|
||||
private lateinit var binding: ActivitySubEditBinding
|
||||
private val binding by lazy { ActivitySubEditBinding.inflate(layoutInflater) }
|
||||
|
||||
var del_config: MenuItem? = null
|
||||
var save_config: MenuItem? = null
|
||||
@@ -33,9 +28,7 @@ class SubEditActivity : BaseActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivitySubEditBinding.inflate(layoutInflater)
|
||||
val view = binding.root
|
||||
setContentView(view)
|
||||
setContentView(binding.root)
|
||||
title = getString(R.string.title_sub_setting)
|
||||
|
||||
val json = subStorage?.decodeString(editSubId)
|
||||
@@ -108,9 +101,13 @@ class SubEditActivity : BaseActivity() {
|
||||
if (editSubId.isNotEmpty()) {
|
||||
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
MmkvManager.removeSubscription(editSubId)
|
||||
launch(Dispatchers.Main) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.no) { _, _ ->
|
||||
// do nothing
|
||||
}
|
||||
@@ -136,10 +133,12 @@ class SubEditActivity : BaseActivity() {
|
||||
deleteServer()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.save_config -> {
|
||||
saveServer()
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,32 @@
|
||||
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
|
||||
private val binding by lazy { ActivitySubSettingBinding.inflate(layoutInflater) }
|
||||
|
||||
var subscriptions: List<Pair<String, SubscriptionItem>> = listOf()
|
||||
private val adapter by lazy { SubSettingRecyclerAdapter(this) }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivitySubSettingBinding.inflate(layoutInflater)
|
||||
val view = binding.root
|
||||
setContentView(view)
|
||||
setContentView(binding.root)
|
||||
|
||||
title = getString(R.string.title_sub_setting)
|
||||
|
||||
@@ -37,9 +43,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 +51,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)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.ListView
|
||||
import java.util.ArrayList
|
||||
import com.v2ray.ang.R
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.ListView
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ActivityTaskerBinding
|
||||
import com.v2ray.ang.util.MmkvManager
|
||||
|
||||
class TaskerActivity : BaseActivity() {
|
||||
private lateinit var binding: ActivityTaskerBinding
|
||||
private val binding by lazy { ActivityTaskerBinding.inflate(layoutInflater) }
|
||||
|
||||
private var listview: ListView? = null
|
||||
private var lstData: ArrayList<String> = ArrayList()
|
||||
@@ -27,9 +25,7 @@ class TaskerActivity : BaseActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityTaskerBinding.inflate(layoutInflater)
|
||||
val view = binding.root
|
||||
setContentView(view)
|
||||
setContentView(binding.root)
|
||||
|
||||
//add def value
|
||||
lstData.add("Default")
|
||||
@@ -41,10 +37,12 @@ class TaskerActivity : BaseActivity() {
|
||||
lstGuid.add(key)
|
||||
}
|
||||
}
|
||||
val adapter = ArrayAdapter(this,
|
||||
android.R.layout.simple_list_item_single_choice, lstData)
|
||||
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()
|
||||
}
|
||||
@@ -90,7 +88,7 @@ class TaskerActivity : BaseActivity() {
|
||||
|
||||
intent.putExtra(AppConfig.TASKER_EXTRA_BUNDLE, extraBundle)
|
||||
intent.putExtra(AppConfig.TASKER_EXTRA_STRING_BLURB, blurb)
|
||||
setResult(AppCompatActivity.RESULT_OK, intent)
|
||||
setResult(RESULT_OK, intent)
|
||||
finish()
|
||||
}
|
||||
|
||||
@@ -105,10 +103,12 @@ class TaskerActivity : BaseActivity() {
|
||||
R.id.del_config -> {
|
||||
true
|
||||
}
|
||||
|
||||
R.id.save_config -> {
|
||||
confirmFinish()
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -11,45 +11,32 @@ import com.v2ray.ang.util.AngConfigManager
|
||||
import java.net.URLDecoder
|
||||
|
||||
class UrlSchemeActivity : BaseActivity() {
|
||||
private lateinit var binding: ActivityLogcatBinding
|
||||
private val binding by lazy { ActivityLogcatBinding.inflate(layoutInflater) }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityLogcatBinding.inflate(layoutInflater)
|
||||
val view = binding.root
|
||||
setContentView(view)
|
||||
setContentView(binding.root)
|
||||
|
||||
try {
|
||||
intent.apply {
|
||||
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, null)
|
||||
}
|
||||
}
|
||||
} 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").orEmpty()
|
||||
parseUri(shareUrl, uri?.fragment)
|
||||
}
|
||||
|
||||
"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").orEmpty()
|
||||
parseUri(shareUrl, uri?.fragment)
|
||||
}
|
||||
|
||||
else -> {
|
||||
@@ -57,10 +44,8 @@ class UrlSchemeActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
startActivity(Intent(this, MainActivity::class.java))
|
||||
finish()
|
||||
} catch (e: Exception) {
|
||||
@@ -68,19 +53,25 @@ 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?, fragment: String?) {
|
||||
if (uriString.isNullOrEmpty()) {
|
||||
return
|
||||
}
|
||||
Log.d("UrlScheme", uriString)
|
||||
|
||||
private fun importConfig(shareUrl: String) {
|
||||
val count = AngConfigManager.importBatchConfig(shareUrl, "", false)
|
||||
if (count > 0) {
|
||||
toast(R.string.toast_success)
|
||||
var decodedUrl = URLDecoder.decode(uriString, "UTF-8")
|
||||
val uri = Uri.parse(decodedUrl)
|
||||
if (uri != null) {
|
||||
if (uri.fragment.isNullOrEmpty() && !fragment.isNullOrEmpty()) {
|
||||
decodedUrl += "#${fragment}"
|
||||
}
|
||||
Log.d("UrlScheme-decodedUrl", decodedUrl)
|
||||
val (count, countSub) = AngConfigManager.importBatchConfig(decodedUrl, "", false)
|
||||
if (count + countSub > 0) {
|
||||
toast(R.string.import_subscription_success)
|
||||
} else {
|
||||
toast(R.string.toast_failure)
|
||||
toast(R.string.import_subscription_failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,26 +2,31 @@ package com.v2ray.ang.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.OpenableColumns
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View.GONE
|
||||
import android.view.View.VISIBLE
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.gson.Gson
|
||||
import com.tbruyelle.rxpermissions.RxPermissions
|
||||
import com.tbruyelle.rxpermissions3.RxPermissions
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ActivitySubSettingBinding
|
||||
import com.v2ray.ang.databinding.ItemRecyclerUserAssetBinding
|
||||
import com.v2ray.ang.databinding.LayoutProgressBinding
|
||||
import com.v2ray.ang.dto.AssetUrlItem
|
||||
import com.v2ray.ang.extension.toTrafficString
|
||||
import com.v2ray.ang.extension.toast
|
||||
@@ -36,10 +41,10 @@ import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
import java.net.URL
|
||||
import java.text.DateFormat
|
||||
import java.util.*
|
||||
import java.util.Date
|
||||
|
||||
class UserAssetActivity : BaseActivity() {
|
||||
private lateinit var binding: ActivitySubSettingBinding
|
||||
private val binding by lazy { ActivitySubSettingBinding.inflate(layoutInflater) }
|
||||
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
|
||||
private val assetStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_ASSET, MMKV.MULTI_PROCESS_MODE) }
|
||||
|
||||
@@ -49,9 +54,7 @@ class UserAssetActivity : BaseActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivitySubSettingBinding.inflate(layoutInflater)
|
||||
val view = binding.root
|
||||
setContentView(view)
|
||||
setContentView(binding.root)
|
||||
title = getString(R.string.title_user_asset_setting)
|
||||
|
||||
binding.recyclerView.setHasFixedSize(true)
|
||||
@@ -80,6 +83,7 @@ class UserAssetActivity : BaseActivity() {
|
||||
startActivity(intent)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.download_file -> {
|
||||
downloadGeoFiles()
|
||||
true
|
||||
@@ -168,9 +172,13 @@ class UserAssetActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
private fun downloadGeoFiles() {
|
||||
val httpPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT), AppConfig.PORT_HTTP.toInt())
|
||||
|
||||
val dialog = AlertDialog.Builder(this)
|
||||
.setView(LayoutProgressBinding.inflate(layoutInflater).root)
|
||||
.setCancelable(false)
|
||||
.show()
|
||||
toast(R.string.msg_downloading_content)
|
||||
|
||||
val httpPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT), AppConfig.PORT_HTTP.toInt())
|
||||
var assets = MmkvManager.decodeAssetUrls()
|
||||
assets = addBuiltInGeoItems(assets)
|
||||
|
||||
@@ -188,6 +196,7 @@ class UserAssetActivity : BaseActivity() {
|
||||
} else {
|
||||
toast(getString(R.string.toast_failure) + " " + it.second.remarks)
|
||||
}
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -229,15 +238,18 @@ class UserAssetActivity : BaseActivity() {
|
||||
conn?.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
private fun addBuiltInGeoItems(assets: List<Pair<String, AssetUrlItem>>): List<Pair<String, AssetUrlItem>> {
|
||||
val list = mutableListOf<Pair<String, AssetUrlItem>>()
|
||||
builtInGeoFiles
|
||||
.filter { geoFile -> assets.none { it.second.remarks == geoFile } }
|
||||
.forEach {
|
||||
list.add(Utils.getUuid() to AssetUrlItem(
|
||||
list.add(
|
||||
Utils.getUuid() to AssetUrlItem(
|
||||
it,
|
||||
AppConfig.GeoUrl + it
|
||||
))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return list + assets
|
||||
@@ -249,14 +261,15 @@ class UserAssetActivity : BaseActivity() {
|
||||
ItemRecyclerUserAssetBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false)
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: UserAssetViewHolder, position: Int) {
|
||||
var assets = MmkvManager.decodeAssetUrls();
|
||||
assets = addBuiltInGeoItems(assets);
|
||||
var assets = MmkvManager.decodeAssetUrls()
|
||||
assets = addBuiltInGeoItems(assets)
|
||||
val item = assets.getOrNull(position) ?: return
|
||||
// file with name == item.second.remarks
|
||||
val file = extDir.listFiles()?.find { it.name == item.second.remarks }
|
||||
@@ -292,8 +305,8 @@ class UserAssetActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
var assets = MmkvManager.decodeAssetUrls();
|
||||
assets = addBuiltInGeoItems(assets);
|
||||
var assets = MmkvManager.decodeAssetUrls()
|
||||
assets = addBuiltInGeoItems(assets)
|
||||
return assets.size
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import com.v2ray.ang.util.Utils
|
||||
import java.io.File
|
||||
|
||||
class UserAssetUrlActivity : BaseActivity() {
|
||||
private lateinit var binding: ActivityUserAssetUrlBinding
|
||||
private val binding by lazy { ActivityUserAssetUrlBinding.inflate(layoutInflater) }
|
||||
|
||||
var del_config: MenuItem? = null
|
||||
var save_config: MenuItem? = null
|
||||
@@ -27,9 +27,7 @@ class UserAssetUrlActivity : BaseActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityUserAssetUrlBinding.inflate(layoutInflater)
|
||||
val view = binding.root
|
||||
setContentView(view)
|
||||
setContentView(binding.root)
|
||||
title = getString(R.string.title_user_asset_add_url)
|
||||
|
||||
val json = assetStorage?.decodeString(editAssetId)
|
||||
@@ -139,10 +137,12 @@ class UserAssetUrlActivity : BaseActivity() {
|
||||
deleteServer()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.save_config -> {
|
||||
saveServer()
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
@@ -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): Pair<Int, 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 to 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
|
||||
@@ -991,7 +434,7 @@ object AngConfigManager {
|
||||
val removedSelectedServer =
|
||||
if (!TextUtils.isEmpty(subid) && !append) {
|
||||
MmkvManager.decodeServerConfig(
|
||||
mainStorage?.decodeString(KEY_SELECTED_SERVER) ?: ""
|
||||
mainStorage?.decodeString(KEY_SELECTED_SERVER).orEmpty()
|
||||
)?.let {
|
||||
if (it.subscriptionId == subid) {
|
||||
return@let it
|
||||
@@ -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())
|
||||
@@ -1094,8 +517,83 @@ object AngConfigManager {
|
||||
val key = MmkvManager.encodeServerConfig("", config)
|
||||
serverRawStorage?.encode(key, server)
|
||||
return 1
|
||||
} else if (server.startsWith("[Interface]") && server.contains("[Peer]")) {
|
||||
val config = WireguardFmt.parseWireguardConfFile(server)
|
||||
?: return R.string.toast_incorrect_protocol
|
||||
config.fullConfig?.remarks ?: System.currentTimeMillis().toString()
|
||||
val key = MmkvManager.encodeServerConfig("", config)
|
||||
serverRawStorage?.encode(key, server)
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fun updateConfigViaSubAll(): Int {
|
||||
var count = 0
|
||||
try {
|
||||
MmkvManager.decodeSubscriptions().forEach {
|
||||
count += updateConfigViaSub(it)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return 0
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,7 @@ import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import com.v2ray.ang.dto.AppInfo
|
||||
import rx.Observable
|
||||
import java.util.*
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.v2ray.ang.util
|
||||
import com.google.gson.Gson
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.dto.AssetUrlItem
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.ServerAffiliationInfo
|
||||
import com.v2ray.ang.dto.ServerConfig
|
||||
import com.v2ray.ang.dto.SubscriptionItem
|
||||
@@ -11,6 +12,7 @@ import java.net.URI
|
||||
object MmkvManager {
|
||||
const val ID_MAIN = "MAIN"
|
||||
const val ID_SERVER_CONFIG = "SERVER_CONFIG"
|
||||
const val ID_PROFILE_CONFIG = "PROFILE_CONFIG"
|
||||
const val ID_SERVER_RAW = "SERVER_RAW"
|
||||
const val ID_SERVER_AFF = "SERVER_AFF"
|
||||
const val ID_SUB = "SUB"
|
||||
@@ -19,11 +21,14 @@ object MmkvManager {
|
||||
const val KEY_SELECTED_SERVER = "SELECTED_SERVER"
|
||||
const val KEY_ANG_CONFIGS = "ANG_CONFIGS"
|
||||
|
||||
private val mainStorage by lazy { MMKV.mmkvWithID(ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
|
||||
val mainStorage by lazy { MMKV.mmkvWithID(ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
|
||||
val settingsStorage by lazy { MMKV.mmkvWithID(ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
|
||||
private val serverStorage by lazy { MMKV.mmkvWithID(ID_SERVER_CONFIG, MMKV.MULTI_PROCESS_MODE) }
|
||||
private val profileStorage by lazy { MMKV.mmkvWithID(ID_PROFILE_CONFIG, MMKV.MULTI_PROCESS_MODE) }
|
||||
private val serverAffStorage by lazy { MMKV.mmkvWithID(ID_SERVER_AFF, MMKV.MULTI_PROCESS_MODE) }
|
||||
private val subStorage by lazy { MMKV.mmkvWithID(ID_SUB, MMKV.MULTI_PROCESS_MODE) }
|
||||
val subStorage by lazy { MMKV.mmkvWithID(ID_SUB, MMKV.MULTI_PROCESS_MODE) }
|
||||
private val assetStorage by lazy { MMKV.mmkvWithID(ID_ASSET, MMKV.MULTI_PROCESS_MODE) }
|
||||
val serverRawStorage by lazy { MMKV.mmkvWithID(ID_SERVER_RAW, MMKV.MULTI_PROCESS_MODE) }
|
||||
|
||||
fun decodeServerList(): MutableList<String> {
|
||||
val json = mainStorage?.decodeString(KEY_ANG_CONFIGS)
|
||||
@@ -45,6 +50,17 @@ object MmkvManager {
|
||||
return Gson().fromJson(json, ServerConfig::class.java)
|
||||
}
|
||||
|
||||
fun decodeProfileConfig(guid: String): ProfileItem? {
|
||||
if (guid.isBlank()) {
|
||||
return null
|
||||
}
|
||||
val json = profileStorage?.decodeString(guid)
|
||||
if (json.isNullOrBlank()) {
|
||||
return null
|
||||
}
|
||||
return Gson().fromJson(json, ProfileItem::class.java)
|
||||
}
|
||||
|
||||
fun encodeServerConfig(guid: String, config: ServerConfig): String {
|
||||
val key = guid.ifBlank { Utils.getUuid() }
|
||||
serverStorage?.encode(key, Gson().toJson(config))
|
||||
@@ -56,6 +72,14 @@ object MmkvManager {
|
||||
mainStorage?.encode(KEY_SELECTED_SERVER, key)
|
||||
}
|
||||
}
|
||||
val profile = ProfileItem(
|
||||
configType = config.configType,
|
||||
subscriptionId = config.subscriptionId,
|
||||
remarks = config.remarks,
|
||||
server = config.getProxyOutbound()?.getServerAddress(),
|
||||
serverPort = config.getProxyOutbound()?.getServerPort(),
|
||||
)
|
||||
profileStorage?.encode(key, Gson().toJson(profile))
|
||||
return key
|
||||
}
|
||||
|
||||
@@ -70,6 +94,7 @@ object MmkvManager {
|
||||
serverList.remove(guid)
|
||||
mainStorage?.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList))
|
||||
serverStorage?.remove(guid)
|
||||
profileStorage?.remove(guid)
|
||||
serverAffStorage?.remove(guid)
|
||||
}
|
||||
|
||||
@@ -124,7 +149,7 @@ object MmkvManager {
|
||||
}
|
||||
val uri = URI(Utils.fixIllegalUrl(url))
|
||||
val subItem = SubscriptionItem()
|
||||
subItem.remarks = Utils.urlDecode(uri.fragment ?: "import sub")
|
||||
subItem.remarks = uri.fragment ?: "import sub"
|
||||
subItem.url = url
|
||||
subStorage?.encode(Utils.getUuid(), Gson().toJson(subItem))
|
||||
return 1
|
||||
@@ -138,8 +163,7 @@ object MmkvManager {
|
||||
subscriptions.add(Pair(key, Gson().fromJson(json, SubscriptionItem::class.java)))
|
||||
}
|
||||
}
|
||||
subscriptions.sortedBy { (_, value) -> value.addedTime }
|
||||
return subscriptions
|
||||
return subscriptions.sortedBy { (_, value) -> value.addedTime }
|
||||
}
|
||||
|
||||
fun removeSubscription(subid: String) {
|
||||
@@ -155,8 +179,7 @@ object MmkvManager {
|
||||
assetUrlItems.add(Pair(key, Gson().fromJson(json, AssetUrlItem::class.java)))
|
||||
}
|
||||
}
|
||||
assetUrlItems.sortedBy { (_, value) -> value.addedTime }
|
||||
return assetUrlItems
|
||||
return assetUrlItems.sortedBy { (_, value) -> value.addedTime }
|
||||
}
|
||||
|
||||
fun removeAssetUrl(assetid: String) {
|
||||
@@ -166,10 +189,18 @@ object MmkvManager {
|
||||
fun removeAllServer() {
|
||||
mainStorage?.clearAll()
|
||||
serverStorage?.clearAll()
|
||||
profileStorage?.clearAll()
|
||||
serverAffStorage?.clearAll()
|
||||
}
|
||||
|
||||
fun removeInvalidServer() {
|
||||
fun removeInvalidServer(guid: String) {
|
||||
if (guid.isNotEmpty()) {
|
||||
decodeServerAffiliationInfo(guid)?.let { aff ->
|
||||
if (aff.testDelayMillis < 0L) {
|
||||
removeServer(guid)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
serverAffStorage?.allKeys()?.forEach { key ->
|
||||
decodeServerAffiliationInfo(key)?.let { aff ->
|
||||
if (aff.testDelayMillis < 0L) {
|
||||
@@ -178,6 +209,7 @@ object MmkvManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun sortByTestResults() {
|
||||
data class ServerDelay(var guid: String, var testDelayMillis: Long)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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.orEmpty())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,15 +63,10 @@ object Utils {
|
||||
}
|
||||
|
||||
fun parseInt(str: String?, default: Int): Int {
|
||||
str ?: return default
|
||||
return try {
|
||||
Integer.parseInt(str)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
default
|
||||
}
|
||||
return str?.toIntOrNull() ?: default
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* get text from clipboard
|
||||
*/
|
||||
@@ -101,23 +96,19 @@ object Utils {
|
||||
/**
|
||||
* base64 decode
|
||||
*/
|
||||
fun decode(text: String): String {
|
||||
tryDecodeBase64(text)?.let { return it }
|
||||
if (text.endsWith('=')) {
|
||||
// try again for some loosely formatted base64
|
||||
tryDecodeBase64(text.trimEnd('='))?.let { return it }
|
||||
}
|
||||
return ""
|
||||
fun decode(text: String?): String {
|
||||
return tryDecodeBase64(text) ?: text?.trimEnd('=')?.let { tryDecodeBase64(it) }.orEmpty()
|
||||
}
|
||||
|
||||
fun tryDecodeBase64(text: String): String? {
|
||||
|
||||
fun tryDecodeBase64(text: String?): String? {
|
||||
try {
|
||||
return Base64.decode(text, Base64.NO_WRAP).toString(charset("UTF-8"))
|
||||
return Base64.decode(text, Base64.NO_WRAP).toString(Charsets.UTF_8)
|
||||
} catch (e: Exception) {
|
||||
Log.i(ANG_PACKAGE, "Parse base64 standard failed $e")
|
||||
}
|
||||
try {
|
||||
return Base64.decode(text, Base64.NO_WRAP.or(Base64.URL_SAFE)).toString(charset("UTF-8"))
|
||||
return Base64.decode(text, Base64.NO_WRAP.or(Base64.URL_SAFE)).toString(Charsets.UTF_8)
|
||||
} catch (e: Exception) {
|
||||
Log.i(ANG_PACKAGE, "Parse base64 url safe failed $e")
|
||||
}
|
||||
@@ -129,7 +120,7 @@ object Utils {
|
||||
*/
|
||||
fun encode(text: String): String {
|
||||
return try {
|
||||
Base64.encodeToString(text.toByteArray(charset("UTF-8")), Base64.NO_WRAP)
|
||||
Base64.encodeToString(text.toByteArray(Charsets.UTF_8), Base64.NO_WRAP)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
""
|
||||
@@ -213,7 +204,8 @@ object Utils {
|
||||
}
|
||||
|
||||
fun isIpv4Address(value: String): Boolean {
|
||||
val regV4 = Regex("^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$")
|
||||
val regV4 =
|
||||
Regex("^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$")
|
||||
return regV4.matches(value)
|
||||
}
|
||||
|
||||
@@ -223,12 +215,13 @@ object Utils {
|
||||
addr = addr.drop(1)
|
||||
addr = addr.dropLast(addr.count() - addr.lastIndexOf("]"))
|
||||
}
|
||||
val regV6 = Regex("^((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7}$")
|
||||
val regV6 =
|
||||
Regex("^((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7}$")
|
||||
return regV6.matches(addr)
|
||||
}
|
||||
|
||||
private fun isCoreDNSAddress(s: String): Boolean {
|
||||
return s.startsWith("https") || s.startsWith("tcp") || s.startsWith("quic")
|
||||
return s.startsWith("https") || s.startsWith("tcp") || s.startsWith("quic") || s == "localhost"
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -236,7 +229,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) {
|
||||
@@ -282,7 +281,7 @@ object Utils {
|
||||
|
||||
fun urlDecode(url: String): String {
|
||||
return try {
|
||||
URLDecoder.decode(url, "UTF-8")
|
||||
URLDecoder.decode(url, Charsets.UTF_8.toString())
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
url
|
||||
@@ -291,7 +290,7 @@ object Utils {
|
||||
|
||||
fun urlEncode(url: String): String {
|
||||
return try {
|
||||
URLEncoder.encode(url, "UTF-8")
|
||||
URLEncoder.encode(url, Charsets.UTF_8.toString())
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
url
|
||||
@@ -302,7 +301,10 @@ 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()
|
||||
}
|
||||
@@ -326,7 +328,7 @@ object Utils {
|
||||
}
|
||||
|
||||
fun getDeviceIdForXUDPBaseKey(): String {
|
||||
val androidId = Settings.Secure.ANDROID_ID.toByteArray(charset("UTF-8"))
|
||||
val androidId = Settings.Secure.ANDROID_ID.toByteArray(Charsets.UTF_8)
|
||||
return Base64.encodeToString(androidId.copyOf(32), Base64.NO_PADDING.or(Base64.URL_SAFE))
|
||||
}
|
||||
|
||||
@@ -352,7 +354,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,11 +366,15 @@ object Utils {
|
||||
)
|
||||
)
|
||||
}
|
||||
conn.connectTimeout = timeout
|
||||
conn.readTimeout = timeout
|
||||
conn.setRequestProperty("Connection", "close")
|
||||
conn.setRequestProperty("User-agent", "v2rayNG/${BuildConfig.VERSION_NAME}")
|
||||
url.userInfo?.let {
|
||||
conn.setRequestProperty("Authorization",
|
||||
"Basic ${encode(urlDecode(it))}")
|
||||
conn.setRequestProperty(
|
||||
"Authorization",
|
||||
"Basic ${encode(urlDecode(it))}"
|
||||
)
|
||||
}
|
||||
conn.useCaches = false
|
||||
return conn.inputStream.use {
|
||||
@@ -377,10 +383,10 @@ object Utils {
|
||||
}
|
||||
|
||||
fun getDarkModeStatus(context: Context): Boolean {
|
||||
val mode = context.resources.configuration.uiMode and UI_MODE_NIGHT_MASK
|
||||
return mode != UI_MODE_NIGHT_NO
|
||||
return context.resources.configuration.uiMode and UI_MODE_NIGHT_MASK != UI_MODE_NIGHT_NO
|
||||
}
|
||||
|
||||
|
||||
fun setNightMode(context: Context) {
|
||||
when (settingsStorage?.decodeString(AppConfig.PREF_UI_MODE_NIGHT, "0")) {
|
||||
"0" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||
@@ -389,7 +395,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 {
|
||||
@@ -397,17 +406,21 @@ object Utils {
|
||||
}
|
||||
}
|
||||
|
||||
fun getLocale(context: Context): Locale =
|
||||
when (settingsStorage?.decodeString(AppConfig.PREF_LANGUAGE) ?: "auto") {
|
||||
fun getLocale(): Locale {
|
||||
val lang = settingsStorage?.decodeString(AppConfig.PREF_LANGUAGE) ?: "auto"
|
||||
return when (lang) {
|
||||
"auto" -> getSysLocale()
|
||||
"en" -> Locale("en")
|
||||
"zh-rCN" -> Locale("zh", "CN")
|
||||
"zh-rTW" -> Locale("zh", "TW")
|
||||
"en" -> Locale.ENGLISH
|
||||
"zh-rCN" -> Locale.CHINA
|
||||
"zh-rTW" -> Locale.TRADITIONAL_CHINESE
|
||||
"vi" -> Locale("vi")
|
||||
"ru" -> Locale("ru")
|
||||
"fa" -> Locale("fa")
|
||||
"bn" -> Locale("bn")
|
||||
else -> getSysLocale()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun getSysLocale(): Locale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
LocaleList.getDefault()[0]
|
||||
@@ -434,9 +447,14 @@ object Utils {
|
||||
fun isTv(context: Context): Boolean =
|
||||
context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
||||
|
||||
fun getDelayTestUrl(): String {
|
||||
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 {
|
||||
settingsStorage.decodeString(AppConfig.PREF_DELAY_TEST_URL) ?: AppConfig.DelayTestUrl
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,16 @@ 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_BLOCKED
|
||||
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
||||
import com.v2ray.ang.AppConfig.TAG_FRAGMENT
|
||||
import com.v2ray.ang.AppConfig.TAG_PROXY
|
||||
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
|
||||
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V6
|
||||
import com.v2ray.ang.dto.EConfigType
|
||||
@@ -49,6 +53,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 +146,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 +175,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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,65 +183,82 @@ object V2rayConfigUtil {
|
||||
*/
|
||||
private fun routing(v2rayConfig: V2rayConfig): Boolean {
|
||||
try {
|
||||
val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE)
|
||||
?: ERoutingMode.BYPASS_LAN_MAINLAND.value
|
||||
|
||||
routingUserRule(
|
||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_BLOCKED)
|
||||
.orEmpty(), TAG_BLOCKED, v2rayConfig
|
||||
)
|
||||
if (routingMode == ERoutingMode.GLOBAL_DIRECT.value) {
|
||||
routingUserRule(
|
||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
|
||||
.orEmpty(), TAG_DIRECT, v2rayConfig
|
||||
)
|
||||
routingUserRule(
|
||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
|
||||
?: "", AppConfig.TAG_PROXY, v2rayConfig
|
||||
.orEmpty(), TAG_PROXY, v2rayConfig
|
||||
)
|
||||
} else {
|
||||
routingUserRule(
|
||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
|
||||
.orEmpty(), TAG_PROXY, v2rayConfig
|
||||
)
|
||||
routingUserRule(
|
||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
|
||||
?: "", AppConfig.TAG_DIRECT, v2rayConfig
|
||||
)
|
||||
routingUserRule(
|
||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_BLOCKED)
|
||||
?: "", AppConfig.TAG_BLOCKED, v2rayConfig
|
||||
.orEmpty(), TAG_DIRECT, v2rayConfig
|
||||
)
|
||||
}
|
||||
|
||||
v2rayConfig.routing.domainStrategy =
|
||||
settingsStorage?.decodeString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY)
|
||||
?: "IPIfNonMatch"
|
||||
// v2rayConfig.routing.domainMatcher = "mph"
|
||||
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")
|
||||
outboundTag = TAG_PROXY,
|
||||
domain = arrayListOf("domain:googleapis.cn", "domain:gstatic.com")
|
||||
)
|
||||
|
||||
when (routingMode) {
|
||||
ERoutingMode.BYPASS_LAN.value -> {
|
||||
routingGeo("ip", "private", AppConfig.TAG_DIRECT, v2rayConfig)
|
||||
routingGeo("", "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)
|
||||
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("", "private", TAG_DIRECT, v2rayConfig)
|
||||
routingGeo("", "cn", TAG_DIRECT, v2rayConfig)
|
||||
v2rayConfig.routing.rules.add(0, googleapisRoute)
|
||||
}
|
||||
|
||||
ERoutingMode.GLOBAL_DIRECT.value -> {
|
||||
val globalDirect = V2rayConfig.RoutingBean.RulesBean(
|
||||
outboundTag = AppConfig.TAG_DIRECT,
|
||||
port = "0-65535"
|
||||
outboundTag = TAG_DIRECT,
|
||||
)
|
||||
if (v2rayConfig.routing.domainStrategy != "IPIfNonMatch") {
|
||||
globalDirect.port = "0-65535"
|
||||
} else {
|
||||
globalDirect.ip = arrayListOf("0.0.0.0/0", "::/0")
|
||||
}
|
||||
v2rayConfig.routing.rules.add(globalDirect)
|
||||
}
|
||||
}
|
||||
|
||||
if (routingMode != ERoutingMode.GLOBAL_DIRECT.value) {
|
||||
v2rayConfig.routing.rules.add(
|
||||
V2rayConfig.RoutingBean.RulesBean(
|
||||
outboundTag = AppConfig.TAG_PROXY,
|
||||
port = "0-65535"
|
||||
))
|
||||
val globalProxy = V2rayConfig.RoutingBean.RulesBean(
|
||||
outboundTag = TAG_PROXY,
|
||||
)
|
||||
if (v2rayConfig.routing.domainStrategy != "IPIfNonMatch") {
|
||||
globalProxy.port = "0-65535"
|
||||
} else {
|
||||
globalProxy.ip = arrayListOf("0.0.0.0/0", "::/0")
|
||||
}
|
||||
v2rayConfig.routing.rules.add(globalProxy)
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
@@ -295,10 +321,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)
|
||||
}
|
||||
}
|
||||
@@ -307,7 +333,7 @@ object V2rayConfigUtil {
|
||||
}
|
||||
}
|
||||
|
||||
private fun userRule2Domian(userRule: String): ArrayList<String> {
|
||||
private fun userRule2Domain(userRule: String): ArrayList<String> {
|
||||
val domain = ArrayList<String>()
|
||||
userRule.split(",").map { it.trim() }.forEach {
|
||||
if (it.startsWith("geosite:") || it.startsWith("domain:")) {
|
||||
@@ -324,13 +350,13 @@ object V2rayConfigUtil {
|
||||
try {
|
||||
if (settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true) {
|
||||
val geositeCn = arrayListOf("geosite:cn")
|
||||
val proxyDomain = userRule2Domian(
|
||||
val proxyDomain = userRule2Domain(
|
||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
|
||||
?: ""
|
||||
.orEmpty()
|
||||
)
|
||||
val directDomain = userRule2Domian(
|
||||
val directDomain = userRule2Domain(
|
||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
|
||||
?: ""
|
||||
.orEmpty()
|
||||
)
|
||||
// fakedns with all domains to make it always top priority
|
||||
v2rayConfig.dns.servers?.add(
|
||||
@@ -397,14 +423,14 @@ object V2rayConfigUtil {
|
||||
|
||||
private fun dns(v2rayConfig: V2rayConfig): Boolean {
|
||||
try {
|
||||
val hosts = mutableMapOf<String, String>()
|
||||
val hosts = mutableMapOf<String, Any>()
|
||||
val servers = ArrayList<Any>()
|
||||
|
||||
//remote Dns
|
||||
val remoteDns = Utils.getRemoteDnsServers()
|
||||
val proxyDomain = userRule2Domian(
|
||||
val proxyDomain = userRule2Domain(
|
||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
|
||||
?: ""
|
||||
.orEmpty()
|
||||
)
|
||||
remoteDns.forEach {
|
||||
servers.add(it)
|
||||
@@ -422,9 +448,9 @@ object V2rayConfigUtil {
|
||||
|
||||
// domestic DNS
|
||||
val domesticDns = Utils.getDomesticDnsServers()
|
||||
val directDomain = userRule2Domian(
|
||||
val directDomain = userRule2Domain(
|
||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
|
||||
?: ""
|
||||
.orEmpty()
|
||||
)
|
||||
val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE)
|
||||
?: ERoutingMode.BYPASS_LAN_MAINLAND.value
|
||||
@@ -443,7 +469,7 @@ object V2rayConfigUtil {
|
||||
)
|
||||
}
|
||||
if (isCnRoutingMode) {
|
||||
val geositeCn = arrayListOf("geosite:cn", "geosite:geolocation-cn")
|
||||
val geositeCn = arrayListOf("geosite:cn")
|
||||
servers.add(
|
||||
V2rayConfig.DnsBean.ServersBean(
|
||||
domesticDns.first(),
|
||||
@@ -457,7 +483,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
|
||||
@@ -466,9 +492,9 @@ object V2rayConfigUtil {
|
||||
}
|
||||
|
||||
//block dns
|
||||
val blkDomain = userRule2Domian(
|
||||
val blkDomain = userRule2Domain(
|
||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_BLOCKED)
|
||||
?: ""
|
||||
.orEmpty()
|
||||
)
|
||||
if (blkDomain.size > 0) {
|
||||
hosts.putAll(blkDomain.map { it to "127.0.0.1" })
|
||||
@@ -477,6 +503,12 @@ object V2rayConfigUtil {
|
||||
// hardcode googleapi rule to fix play store problems
|
||||
hosts["domain:googleapis.cn"] = "googleapis.com"
|
||||
|
||||
// hardcode popular Android Private DNS rule to fix localhost DNS problem
|
||||
hosts["dns.pub"] = arrayListOf("1.12.12.12", "120.53.53.53")
|
||||
hosts["dns.alidns.com"] = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1")
|
||||
hosts["one.one.one.one"] = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001")
|
||||
hosts["dns.google"] = arrayListOf("8.8.8.8", "8.8.4.4", "2001:4860:4860::8888", "2001:4860:4860::8844")
|
||||
|
||||
// DNS dns对象
|
||||
v2rayConfig.dns = V2rayConfig.DnsBean(
|
||||
servers = servers,
|
||||
@@ -487,7 +519,7 @@ object V2rayConfigUtil {
|
||||
if (Utils.isPureIpAddress(remoteDns.first())) {
|
||||
v2rayConfig.routing.rules.add(
|
||||
0, V2rayConfig.RoutingBean.RulesBean(
|
||||
outboundTag = AppConfig.TAG_PROXY,
|
||||
outboundTag = TAG_PROXY,
|
||||
port = "53",
|
||||
ip = arrayListOf(remoteDns.first()),
|
||||
domain = null
|
||||
@@ -560,7 +592,7 @@ object V2rayConfigUtil {
|
||||
} else {
|
||||
path
|
||||
}
|
||||
outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host = host!!
|
||||
outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host = host
|
||||
}
|
||||
|
||||
|
||||
@@ -589,7 +621,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"
|
||||
) {
|
||||
@@ -607,7 +640,11 @@ object V2rayConfigUtil {
|
||||
?: "50-100",
|
||||
interval = settingsStorage?.decodeString(AppConfig.PREF_FRAGMENT_INTERVAL)
|
||||
?: "10-20"
|
||||
)
|
||||
),
|
||||
noise = V2rayConfig.OutboundBean.OutSettingsBean.NoiseBean(
|
||||
packet = "rand:100-200",
|
||||
delay = "10-20",
|
||||
),
|
||||
)
|
||||
fragmentOutbound.streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean(
|
||||
sockopt = V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean(
|
||||
|
||||
@@ -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.orEmpty())
|
||||
|
||||
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.orEmpty())
|
||||
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.orEmpty(), 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
180
V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/TrojanFmt.kt
Normal file
180
V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/TrojanFmt.kt
Normal 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.orEmpty())
|
||||
|
||||
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"].orEmpty()
|
||||
allowInsecure = if ((queryParam["allowInsecure"].orEmpty()) == "1") true else allowInsecure
|
||||
config.outboundBean?.streamSettings?.populateTlsSettings(
|
||||
queryParam["security"] ?: V2rayConfig.TLS,
|
||||
allowInsecure,
|
||||
queryParam["sni"] ?: sni.orEmpty(),
|
||||
fingerprint,
|
||||
queryParam["alpn"],
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
flow = queryParam["flow"].orEmpty()
|
||||
}
|
||||
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.orEmpty()
|
||||
}
|
||||
if (!TextUtils.isEmpty(tlsSetting.publicKey)) {
|
||||
dicQuery["pbk"] = tlsSetting.publicKey.orEmpty()
|
||||
}
|
||||
if (!TextUtils.isEmpty(tlsSetting.shortId)) {
|
||||
dicQuery["sid"] = tlsSetting.shortId.orEmpty()
|
||||
}
|
||||
if (!TextUtils.isEmpty(tlsSetting.spiderX)) {
|
||||
dicQuery["spx"] = Utils.urlEncode(tlsSetting.spiderX.orEmpty())
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
171
V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/VlessFmt.kt
Normal file
171
V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/VlessFmt.kt
Normal 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.orEmpty())
|
||||
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"].orEmpty()
|
||||
}
|
||||
|
||||
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"].orEmpty()) == "1") true else allowInsecure
|
||||
streamSetting.populateTlsSettings(
|
||||
queryParam["security"].orEmpty(),
|
||||
allowInsecure,
|
||||
queryParam["sni"] ?: sni,
|
||||
queryParam["fp"].orEmpty(),
|
||||
queryParam["alpn"],
|
||||
queryParam["pbk"].orEmpty(),
|
||||
queryParam["sid"].orEmpty(),
|
||||
queryParam["spx"].orEmpty()
|
||||
)
|
||||
|
||||
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.orEmpty()
|
||||
}
|
||||
if (!TextUtils.isEmpty(tlsSetting.publicKey)) {
|
||||
dicQuery["pbk"] = tlsSetting.publicKey.orEmpty()
|
||||
}
|
||||
if (!TextUtils.isEmpty(tlsSetting.shortId)) {
|
||||
dicQuery["sid"] = tlsSetting.shortId.orEmpty()
|
||||
}
|
||||
if (!TextUtils.isEmpty(tlsSetting.spiderX)) {
|
||||
dicQuery["spx"] = Utils.urlEncode(tlsSetting.spiderX.orEmpty())
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
160
V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/VmessFmt.kt
Normal file
160
V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/VmessFmt.kt
Normal 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.orEmpty())
|
||||
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"].orEmpty()) == "1") true else allowInsecure
|
||||
streamSetting.populateTlsSettings(
|
||||
queryParam["security"].orEmpty(),
|
||||
allowInsecure,
|
||||
queryParam["sni"] ?: sni,
|
||||
queryParam["fp"].orEmpty(),
|
||||
queryParam["alpn"],
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
|
||||
return config
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
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.orEmpty())
|
||||
|
||||
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"].orEmpty()
|
||||
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 parseWireguardConfFile(str: String): ServerConfig? {
|
||||
val config = ServerConfig.create(EConfigType.WIREGUARD)
|
||||
val queryParam: MutableMap<String, String> = mutableMapOf()
|
||||
|
||||
var currentSection: String? = null
|
||||
|
||||
str.lines().forEach { line ->
|
||||
val trimmedLine = line.trim()
|
||||
|
||||
when {
|
||||
trimmedLine.startsWith("[Interface]", ignoreCase = true) -> currentSection = "Interface"
|
||||
trimmedLine.startsWith("[Peer]", ignoreCase = true) -> currentSection = "Peer"
|
||||
trimmedLine.isBlank() || trimmedLine.startsWith("#") -> Unit // Skip blank lines or comments
|
||||
currentSection != null -> {
|
||||
val (key, value) = trimmedLine.split("=").map { it.trim() }
|
||||
queryParam[key.lowercase()] = value // Store the key in lowercase for case-insensitivity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
config.outboundBean?.settings?.let { wireguard ->
|
||||
wireguard.secretKey = queryParam["privatekey"].orEmpty()
|
||||
wireguard.address = (queryParam["address"] ?: AppConfig.WIREGUARD_LOCAL_ADDRESS_V4).removeWhiteSpace().split(",")
|
||||
wireguard.peers?.getOrNull(0)?.publicKey = queryParam["publickey"].orEmpty()
|
||||
wireguard.peers?.getOrNull(0)?.endpoint = queryParam["endpoint"].orEmpty()
|
||||
wireguard.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
|
||||
wireguard.reserved = (queryParam["reserved"] ?: "0,0,0").removeWhiteSpace().split(",").map { it.toInt() }
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -3,32 +3,32 @@ package com.v2ray.ang.viewmodel
|
||||
import android.app.Application
|
||||
import android.content.BroadcastReceiver
|
||||
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
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.google.gson.Gson
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AngApplication
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.DialogConfigFilterBinding
|
||||
import com.v2ray.ang.dto.EConfigType
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.ServerConfig
|
||||
import com.v2ray.ang.dto.ServersCache
|
||||
import com.v2ray.ang.dto.SubscriptionItem
|
||||
import com.v2ray.ang.dto.V2rayConfig
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.util.AngConfigManager
|
||||
import com.v2ray.ang.util.AngConfigManager.updateConfigViaSub
|
||||
import com.v2ray.ang.util.MessageUtil
|
||||
import com.v2ray.ang.util.MmkvManager
|
||||
import com.v2ray.ang.util.MmkvManager.KEY_ANG_CONFIGS
|
||||
import com.v2ray.ang.util.MmkvManager.subStorage
|
||||
import com.v2ray.ang.util.SpeedtestUtil
|
||||
import com.v2ray.ang.util.Utils
|
||||
import com.v2ray.ang.util.V2rayConfigUtil
|
||||
@@ -37,37 +37,20 @@ 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) {
|
||||
private val mainStorage by lazy {
|
||||
MMKV.mmkvWithID(
|
||||
MmkvManager.ID_MAIN,
|
||||
MMKV.MULTI_PROCESS_MODE
|
||||
)
|
||||
}
|
||||
private val serverRawStorage by lazy {
|
||||
MMKV.mmkvWithID(
|
||||
MmkvManager.ID_SERVER_RAW,
|
||||
MMKV.MULTI_PROCESS_MODE
|
||||
)
|
||||
}
|
||||
private val settingsStorage by lazy {
|
||||
MMKV.mmkvWithID(
|
||||
MmkvManager.ID_SETTING,
|
||||
MMKV.MULTI_PROCESS_MODE
|
||||
)
|
||||
}
|
||||
private var serverList = MmkvManager.decodeServerList()
|
||||
var subscriptionId: String = MmkvManager.settingsStorage.decodeString(AppConfig.CACHE_SUBSCRIPTION_ID, "").orEmpty()
|
||||
|
||||
var serverList = MmkvManager.decodeServerList()
|
||||
var subscriptionId: String = settingsStorage.decodeString(AppConfig.CACHE_SUBSCRIPTION_ID, "")!!
|
||||
var keywordFilter: String = settingsStorage.decodeString(AppConfig.CACHE_KEYWORD_FILTER, "")!!
|
||||
private set
|
||||
//var keywordFilter: String = MmkvManager.settingsStorage.decodeString(AppConfig.CACHE_KEYWORD_FILTER, "")?:""
|
||||
var keywordFilter = ""
|
||||
val serversCache = mutableListOf<ServersCache>()
|
||||
val isRunning by lazy { MutableLiveData<Boolean>() }
|
||||
val updateListAction by lazy { MutableLiveData<Int>() }
|
||||
val updateTestResultAction by lazy { MutableLiveData<String>() }
|
||||
|
||||
private val tcpingTestScope by lazy { CoroutineScope(Dispatchers.IO) }
|
||||
|
||||
fun startListenBroadcast() {
|
||||
@@ -110,38 +93,97 @@ 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)
|
||||
config.remarks = config.fullConfig?.remarks ?: System.currentTimeMillis().toString()
|
||||
val key = MmkvManager.encodeServerConfig("", config)
|
||||
serverRawStorage?.encode(key, server)
|
||||
MmkvManager.serverRawStorage?.encode(key, server)
|
||||
serverList.add(0, key)
|
||||
serversCache.add(0, ServersCache(key, config))
|
||||
val profile = ProfileItem(
|
||||
configType = config.configType,
|
||||
subscriptionId = config.subscriptionId,
|
||||
remarks = config.remarks,
|
||||
server = config.getProxyOutbound()?.getServerAddress(),
|
||||
serverPort = config.getProxyOutbound()?.getServerPort(),
|
||||
)
|
||||
serversCache.add(0, ServersCache(key, profile))
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun swapServer(fromPosition: Int, toPosition: Int) {
|
||||
Collections.swap(serverList, fromPosition, toPosition)
|
||||
Collections.swap(serversCache, fromPosition, toPosition)
|
||||
mainStorage?.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList))
|
||||
MmkvManager.mainStorage?.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList))
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun updateCache() {
|
||||
serversCache.clear()
|
||||
for (guid in serverList) {
|
||||
var profile = MmkvManager.decodeProfileConfig(guid)
|
||||
if (profile == null) {
|
||||
val config = MmkvManager.decodeServerConfig(guid) ?: continue
|
||||
if (subscriptionId.isNotEmpty() && subscriptionId != config.subscriptionId) {
|
||||
profile = ProfileItem(
|
||||
configType = config.configType,
|
||||
subscriptionId = config.subscriptionId,
|
||||
remarks = config.remarks,
|
||||
server = config.getProxyOutbound()?.getServerAddress(),
|
||||
serverPort = config.getProxyOutbound()?.getServerPort(),
|
||||
)
|
||||
MmkvManager.encodeServerConfig(guid, config)
|
||||
}
|
||||
|
||||
if (subscriptionId.isNotEmpty() && subscriptionId != profile.subscriptionId) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (keywordFilter.isEmpty() || config.remarks.contains(keywordFilter)) {
|
||||
serversCache.add(ServersCache(guid, config))
|
||||
if (keywordFilter.isEmpty() || profile.remarks.contains(keywordFilter)) {
|
||||
serversCache.add(ServersCache(guid, profile))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateConfigViaSubAll(): Int {
|
||||
if (subscriptionId.isNullOrEmpty()) {
|
||||
return AngConfigManager.updateConfigViaSubAll()
|
||||
} else {
|
||||
val json = subStorage?.decodeString(subscriptionId)
|
||||
if (!json.isNullOrBlank()) {
|
||||
return updateConfigViaSub(Pair(subscriptionId, Gson().fromJson(json, SubscriptionItem::class.java)))
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun exportAllServer(): Int {
|
||||
val serverListCopy =
|
||||
if (subscriptionId.isNullOrEmpty() && keywordFilter.isNullOrEmpty()) {
|
||||
serverList
|
||||
} else {
|
||||
serversCache.map { it.guid }.toList()
|
||||
}
|
||||
|
||||
val ret = AngConfigManager.shareNonCustomConfigsToClipboard(
|
||||
getApplication<AngApplication>(),
|
||||
serverListCopy
|
||||
)
|
||||
return ret
|
||||
}
|
||||
|
||||
|
||||
fun testAllTcping() {
|
||||
tcpingTestScope.coroutineContext[Job]?.cancelChildren()
|
||||
SpeedtestUtil.closeAllTcpSockets()
|
||||
@@ -150,9 +192,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
getApplication<AngApplication>().toast(R.string.connection_test_testing)
|
||||
for (item in serversCache) {
|
||||
item.config.getProxyOutbound()?.let { outbound ->
|
||||
val serverAddress = outbound.getServerAddress()
|
||||
val serverPort = outbound.getServerPort()
|
||||
item.profile.let { outbound ->
|
||||
val serverAddress = outbound.server
|
||||
val serverPort = outbound.serverPort
|
||||
if (serverAddress != null && serverPort != null) {
|
||||
tcpingTestScope.launch {
|
||||
val testResult = SpeedtestUtil.tcping(serverAddress, serverPort)
|
||||
@@ -192,60 +234,30 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
MessageUtil.sendMsg2Service(getApplication(), AppConfig.MSG_MEASURE_DELAY, "")
|
||||
}
|
||||
|
||||
fun filterConfig(context: Context) {
|
||||
val subscriptions = MmkvManager.decodeSubscriptions()
|
||||
val listId = subscriptions.map { it.first }.toList().toMutableList()
|
||||
val listRemarks = subscriptions.map { it.second.remarks }.toList().toMutableList()
|
||||
listRemarks += context.getString(R.string.filter_config_all)
|
||||
val checkedItem = if (subscriptionId.isNotEmpty()) {
|
||||
listId.indexOf(subscriptionId)
|
||||
} else {
|
||||
listRemarks.count() - 1
|
||||
}
|
||||
|
||||
val ivBinding = DialogConfigFilterBinding.inflate(LayoutInflater.from(context))
|
||||
ivBinding.spSubscriptionId.adapter = ArrayAdapter<String>(
|
||||
context,
|
||||
android.R.layout.simple_spinner_dropdown_item,
|
||||
listRemarks
|
||||
)
|
||||
ivBinding.spSubscriptionId.setSelection(checkedItem)
|
||||
ivBinding.etKeyword.text = Utils.getEditable(keywordFilter)
|
||||
val builder = AlertDialog.Builder(context).setView(ivBinding.root)
|
||||
builder.setTitle(R.string.title_filter_config)
|
||||
builder.setPositiveButton(R.string.tasker_setting_confirm) { dialogInterface: DialogInterface?, _: Int ->
|
||||
try {
|
||||
val position = ivBinding.spSubscriptionId.selectedItemPosition
|
||||
subscriptionId = if (listRemarks.count() - 1 == position) {
|
||||
""
|
||||
} else {
|
||||
subscriptions[position].first
|
||||
}
|
||||
keywordFilter = ivBinding.etKeyword.text.toString()
|
||||
settingsStorage?.encode(AppConfig.CACHE_SUBSCRIPTION_ID, subscriptionId)
|
||||
settingsStorage?.encode(AppConfig.CACHE_KEYWORD_FILTER, keywordFilter)
|
||||
fun subscriptionIdChanged(id: String) {
|
||||
if (subscriptionId != id) {
|
||||
subscriptionId = id
|
||||
MmkvManager.settingsStorage.encode(AppConfig.CACHE_SUBSCRIPTION_ID, subscriptionId)
|
||||
reloadServerList()
|
||||
}
|
||||
}
|
||||
|
||||
dialogInterface?.dismiss()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
fun getSubscriptions(context: Context): Pair<MutableList<String>?, MutableList<String>?> {
|
||||
val subscriptions = MmkvManager.decodeSubscriptions()
|
||||
if (subscriptionId.isNotEmpty()
|
||||
&& !subscriptions.map { it.first }.contains(subscriptionId)
|
||||
) {
|
||||
subscriptionIdChanged("")
|
||||
}
|
||||
if (subscriptions.isEmpty()) {
|
||||
return null to null
|
||||
}
|
||||
builder.show()
|
||||
// AlertDialog.Builder(context)
|
||||
// .setSingleChoiceItems(listRemarks.toTypedArray(), checkedItem) { dialog, i ->
|
||||
// try {
|
||||
// subscriptionId = if (listRemarks.count() - 1 == i) {
|
||||
// ""
|
||||
// } else {
|
||||
// subscriptions[i].first
|
||||
// }
|
||||
// reloadServerList()
|
||||
// dialog.dismiss()
|
||||
// } catch (e: Exception) {
|
||||
// e.printStackTrace()
|
||||
// }
|
||||
// }.show()
|
||||
val listId = subscriptions.map { it.first }.toMutableList()
|
||||
listId.add(0, "")
|
||||
val listRemarks = subscriptions.map { it.second.remarks }.toMutableList()
|
||||
listRemarks.add(0, context.getString(R.string.filter_config_all))
|
||||
|
||||
return listId to listRemarks
|
||||
}
|
||||
|
||||
fun getPosition(guid: String): Int {
|
||||
@@ -256,15 +268,21 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
return -1
|
||||
}
|
||||
|
||||
fun removeDuplicateServer() {
|
||||
fun removeDuplicateServer(): Int {
|
||||
val serversCacheCopy = mutableListOf<Pair<String, ServerConfig>>()
|
||||
for (it in serversCache) {
|
||||
val config = MmkvManager.decodeServerConfig(it.guid) ?: continue
|
||||
serversCacheCopy.add(Pair(it.guid, config))
|
||||
}
|
||||
|
||||
val deleteServer = mutableListOf<String>()
|
||||
serversCache.forEachIndexed { index, it ->
|
||||
val outbound = it.config.getProxyOutbound()
|
||||
serversCache.forEachIndexed { index2, it2 ->
|
||||
serversCacheCopy.forEachIndexed { index, it ->
|
||||
val outbound = it.second.getProxyOutbound()
|
||||
serversCacheCopy.forEachIndexed { index2, it2 ->
|
||||
if (index2 > index) {
|
||||
val outbound2 = it2.config.getProxyOutbound()
|
||||
if (outbound == outbound2 && !deleteServer.contains(it2.guid)) {
|
||||
deleteServer.add(it2.guid)
|
||||
val outbound2 = it2.second.getProxyOutbound()
|
||||
if (outbound == outbound2 && !deleteServer.contains(it2.first)) {
|
||||
deleteServer.add(it2.first)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -272,13 +290,70 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
for (it in deleteServer) {
|
||||
MmkvManager.removeServer(it)
|
||||
}
|
||||
|
||||
return deleteServer.count()
|
||||
}
|
||||
|
||||
fun removeAllServer() {
|
||||
if (subscriptionId.isNullOrEmpty() && keywordFilter.isNullOrEmpty()) {
|
||||
MmkvManager.removeAllServer()
|
||||
} else {
|
||||
val serversCopy = serversCache.toList()
|
||||
for (item in serversCopy) {
|
||||
MmkvManager.removeServer(item.guid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeInvalidServer() {
|
||||
if (subscriptionId.isNullOrEmpty() && keywordFilter.isNullOrEmpty()) {
|
||||
MmkvManager.removeInvalidServer("")
|
||||
} else {
|
||||
val serversCopy = serversCache.toList()
|
||||
for (item in serversCopy) {
|
||||
MmkvManager.removeInvalidServer(item.guid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun sortByTestResults() {
|
||||
MmkvManager.sortByTestResults()
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun filterConfig(keyword: String) {
|
||||
if (keyword == keywordFilter) {
|
||||
return
|
||||
}
|
||||
keywordFilter = keyword
|
||||
MmkvManager.settingsStorage.encode(AppConfig.CACHE_KEYWORD_FILTER, keywordFilter)
|
||||
reloadServerList()
|
||||
getApplication<AngApplication>().toast(
|
||||
getApplication<AngApplication>().getString(
|
||||
R.string.title_del_duplicate_config_count,
|
||||
deleteServer.count()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private val mMsgReceiver = object : BroadcastReceiver() {
|
||||
@@ -311,7 +386,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
}
|
||||
|
||||
AppConfig.MSG_MEASURE_CONFIG_SUCCESS -> {
|
||||
val resultPair = intent.getSerializableExtra("content") as Pair<String, Long>
|
||||
val resultPair: Pair<String, Long> = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
intent.getSerializableExtra("content", Pair::class.java) as Pair<String, Long>
|
||||
} else {
|
||||
intent.getSerializableExtra("content") as Pair<String, Long>
|
||||
}
|
||||
MmkvManager.encodeServerTestDelayMillis(resultPair.first, resultPair.second)
|
||||
updateListAction.value = getPosition(resultPair.first)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M7,6h10l-5.01,6.3L7,6zM4.25,5.61C6.27,8.2 10,13 10,13v6c0,0.55 0.45,1 1,1h2c0.55,0 1,-0.45 1,-1v-6c0,0 3.72,-4.8 5.74,-7.39C20.25,4.95 19.78,4 18.95,4H5.04C4.21,4 3.74,4.95 4.25,5.61z"/>
|
||||
<vector android:height="24dp"
|
||||
android:tint="#FFFFFF"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24"
|
||||
android:width="24dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M7,6h10l-5.01,6.3L7,6zM4.25,5.61C6.27,8.2 10,13 10,13v6c0,0.55 0.45,1 1,1h2c0.55,0 1,-0.45 1,-1v-6c0,0 3.72,-4.8 5.74,-7.39C20.25,4.95 19.78,4 18.95,4H5.04C4.21,4 3.74,4.95 4.25,5.61z" />
|
||||
</vector>
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M7,6h10l-5.01,6.3L7,6zM4.25,5.61C6.27,8.2 10,13 10,13v6c0,0.55 0.45,1 1,1h2c0.55,0 1,-0.45 1,-1v-6c0,0 3.72,-4.8 5.74,-7.39C20.25,4.95 19.78,4 18.95,4H5.04C4.21,4 3.74,4.95 4.25,5.61z"/>
|
||||
<vector android:height="24dp"
|
||||
android:tint="#000000"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24"
|
||||
android:width="24dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M7,6h10l-5.01,6.3L7,6zM4.25,5.61C6.27,8.2 10,13 10,13v6c0,0.55 0.45,1 1,1h2c0.55,0 1,-0.45 1,-1v-6c0,0 3.72,-4.8 5.74,-7.39C20.25,4.95 19.78,4 18.95,4H5.04C4.21,4 3.74,4.95 4.25,5.61z" />
|
||||
</vector>
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
<vector android:height="24dp" android:viewportHeight="1024"
|
||||
android:viewportWidth="1024" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M469.2,802.1l-81.7,-24.6L554.8,221.9l81.7,24.6L469.2,802.1zM362.7,654.5l-124.7,-141.7 124.8,-143.5 -64.4,-56 -173.8,199.8 174,197.7 64.1,-56.3zM899.4,513.1l-173.8,-199.8 -64.4,56 124.8,143.5 -124.7,141.7 64.1,56.4 174,-197.7z"/>
|
||||
<vector android:height="24dp"
|
||||
android:viewportHeight="1024"
|
||||
android:viewportWidth="1024"
|
||||
android:width="24dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M469.2,802.1l-81.7,-24.6L554.8,221.9l81.7,24.6L469.2,802.1zM362.7,654.5l-124.7,-141.7 124.8,-143.5 -64.4,-56 -173.8,199.8 174,197.7 64.1,-56.3zM899.4,513.1l-173.8,-199.8 -64.4,56 124.8,143.5 -124.7,141.7 64.1,56.4 174,-197.7z" />
|
||||
</vector>
|
||||
|
||||
@@ -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">
|
||||
@@ -223,4 +226,5 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
|
||||
@@ -37,6 +37,22 @@
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/pb_waiting"
|
||||
app:indicatorColor="@color/color_fab_active"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true"
|
||||
android:visibility="invisible" />
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/tab_group"
|
||||
app:tabMode="scrollable"
|
||||
app:tabTextAppearance="@style/TabLayoutTextStyle"
|
||||
app:tabIndicatorFullWidth="false"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="match_parent" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
@@ -68,7 +84,6 @@
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/fabProgressCircle"
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
</RelativeLayout>
|
||||
android:layout_height="match_parent"></RelativeLayout>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -133,16 +133,19 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_reserved1"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="@dimen/edit_height"
|
||||
android:inputType="number" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_reserved2"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="@dimen/edit_height"
|
||||
android:inputType="number" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_reserved3"
|
||||
android:layout_width="60dp"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
||||
@@ -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">
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
16
V2rayNG/app/src/main/res/layout/layout_progress.xml
Normal file
16
V2rayNG/app/src/main/res/layout/layout_progress.xml
Normal 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>
|
||||
@@ -1,6 +1,5 @@
|
||||
<?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"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/nav_header_height"
|
||||
android:background="@drawable/nav_header_bg"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/activity_horizontal_margin"
|
||||
@@ -27,6 +26,7 @@
|
||||
android:nextFocusDown="@+id/et_sni" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/l2"
|
||||
android:layout_width="match_parent"
|
||||
@@ -47,6 +47,7 @@
|
||||
android:nextFocusDown="@+id/sp_stream_fingerprint" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/l3"
|
||||
android:layout_width="match_parent"
|
||||
@@ -67,6 +68,7 @@
|
||||
android:nextFocusDown="@+id/sp_stream_alpn" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/l4"
|
||||
android:layout_width="match_parent"
|
||||
@@ -87,6 +89,7 @@
|
||||
android:nextFocusDown="@+id/sp_allow_insecure" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/l5"
|
||||
android:layout_width="match_parent"
|
||||
@@ -104,6 +107,7 @@
|
||||
android:layout_height="@dimen/edit_height"
|
||||
android:entries="@array/allowinsecures" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/l6"
|
||||
android:layout_width="match_parent"
|
||||
@@ -124,6 +128,7 @@
|
||||
android:nextFocusDown="@+id/sp_stream_fingerprint" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/l7"
|
||||
android:layout_width="match_parent"
|
||||
@@ -144,6 +149,7 @@
|
||||
android:nextFocusDown="@+id/sp_stream_fingerprint" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/l8"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -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>
|
||||
@@ -3,8 +3,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:showIn="navigation_view">
|
||||
|
||||
<group
|
||||
android:id="@+id/group_main">
|
||||
<group android:id="@+id/group_main">
|
||||
<item
|
||||
android:id="@+id/sub_setting"
|
||||
android:icon="@drawable/ic_subscriptions_24dp"
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/search_view"
|
||||
android:icon="@drawable/ic_outline_filter_alt_24"
|
||||
android:title="@string/menu_item_search"
|
||||
app:actionViewClass="androidx.appcompat.widget.SearchView"
|
||||
app:showAsAction="always|collapseActionView" />
|
||||
<item
|
||||
android:icon="@drawable/ic_add_24dp"
|
||||
android:title="@string/menu_item_add_config"
|
||||
@@ -63,11 +69,6 @@
|
||||
</item>
|
||||
</menu>
|
||||
</item>
|
||||
<item
|
||||
android:id="@+id/filter_config"
|
||||
android:icon="@drawable/ic_outline_filter_alt_24"
|
||||
android:title="@string/title_filter_config"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/service_restart"
|
||||
android:title="@string/title_service_restart"
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_banner_background" />
|
||||
<foreground>
|
||||
<inset android:drawable="@mipmap/ic_banner_foreground" android:inset="10%"/>
|
||||
<inset
|
||||
android:drawable="@mipmap/ic_banner_foreground"
|
||||
android:inset="10%" />
|
||||
</foreground>
|
||||
</adaptive-icon>
|
||||
@@ -37,6 +37,7 @@
|
||||
<string name="menu_item_import_config_custom_url">استيراد تكوين مخصص من عنوان URL</string>
|
||||
<string name="menu_item_import_config_custom_url_scan">استيراد تكوين مخصص مسح عنوان URL</string>
|
||||
<string name="del_config_comfirm">تأكيد الحذف؟</string>
|
||||
<string name="del_invalid_config_comfirm">Please test before deleting! Confirm delete ?</string>
|
||||
<string name="server_lab_remarks">ملاحظات</string>
|
||||
<string name="server_lab_address">العنوان</string>
|
||||
<string name="server_lab_port">المنفذ</string>
|
||||
@@ -48,8 +49,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>
|
||||
|
||||
307
V2rayNG/app/src/main/res/values-bn/strings.xml
Normal file
307
V2rayNG/app/src/main/res/values-bn/strings.xml
Normal file
@@ -0,0 +1,307 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name" translatable="false">v2rayNG</string>
|
||||
<string name="app_widget_name">সুইচ</string>
|
||||
<string name="app_tile_name">সুইচ</string>
|
||||
<string name="app_tile_first_use">এই ফিচারটি প্রথম ব্যবহার করা হচ্ছে, সার্ভার যোগ করতে অ্যাপটি ব্যবহার করুন</string>
|
||||
<string name="navigation_drawer_open">নেভিগেশন ড্রয়ার খোলুন</string>
|
||||
<string name="navigation_drawer_close">নেভিগেশন ড্রয়ার বন্ধ করুন</string>
|
||||
<string name="migration_success">ডেটা স্থানান্তর সফল!</string>
|
||||
<string name="migration_fail">ডেটা স্থানান্তর ব্যর্থ!</string>
|
||||
|
||||
<!-- Notifications -->
|
||||
<string name="notification_action_stop_v2ray">বন্ধ করুন</string>
|
||||
<string name="toast_permission_denied">অনুমতি পাওয়া যাচ্ছে না</string>
|
||||
<string name="notification_action_more">আরও দেখতে ক্লিক করুন</string>
|
||||
<string name="toast_services_start">সার্ভিস শুরু করুন</string>
|
||||
<string name="toast_services_stop">সার্ভিস বন্ধ করুন</string>
|
||||
<string name="toast_services_success">সার্ভিস সফলভাবে শুরু হয়েছে</string>
|
||||
<string name="toast_services_failure">সার্ভিস শুরু হতে ব্যর্থ হয়েছে</string>
|
||||
|
||||
<!--ServerActivity-->
|
||||
<string name="title_server">কনফিগারেশন ফাইল</string>
|
||||
<string name="menu_item_add_config">কনফিগারেশন যোগ করুন</string>
|
||||
<string name="menu_item_save_config">কনফিগারেশন সংরক্ষণ করুন</string>
|
||||
<string name="menu_item_del_config">কনফিগারেশন মুছুন</string>
|
||||
<string name="menu_item_import_config_qrcode">QR কোড থেকে কনফিগারেশন আমদানি করুন</string>
|
||||
<string name="menu_item_import_config_clipboard">ক্লিপবোর্ড থেকে কনফিগারেশন আমদানি করুন</string>
|
||||
<string name="menu_item_import_config_manually_vmess">ম্যানুয়ালি টাইপ করুন [Vmess]</string>
|
||||
<string name="menu_item_import_config_manually_vless">ম্যানুয়ালি টাইপ করুন [VLESS]</string>
|
||||
<string name="menu_item_import_config_manually_ss">ম্যানুয়ালি টাইপ করুন [Shadowsocks]</string>
|
||||
<string name="menu_item_import_config_manually_socks">ম্যানুয়ালি টাইপ করুন [Socks]</string>
|
||||
<string name="menu_item_import_config_manually_trojan">ম্যানুয়ালি টাইপ করুন [Trojan]</string>
|
||||
<string name="menu_item_import_config_manually_wireguard">ম্যানুয়ালি টাইপ করুন [Wireguard]</string>
|
||||
<string name="menu_item_import_config_custom">কাস্টম কনফিগারেশন</string>
|
||||
<string name="menu_item_import_config_custom_clipboard">ক্লিপবোর্ড থেকে কাস্টম কনফিগারেশন আমদানি করুন</string>
|
||||
<string name="menu_item_import_config_custom_local">স্থানীয়ভাবে কাস্টম কনফিগারেশন আমদানি করুন</string>
|
||||
<string name="menu_item_import_config_custom_url">URL থেকে কাস্টম কনফিগারেশন আমদানি করুন</string>
|
||||
<string name="menu_item_import_config_custom_url_scan">কাস্টম কনফিগারেশন স্ক্যান URL আমদানি করুন</string>
|
||||
<string name="del_config_comfirm">মুছে ফেলুন নিশ্চিত করুন?</string>
|
||||
<string name="del_invalid_config_comfirm">Please test before deleting! Confirm delete ?</string>
|
||||
<string name="server_lab_remarks">মন্তব্য</string>
|
||||
<string name="server_lab_address">ঠিকানা</string>
|
||||
<string name="server_lab_port">পোর্ট</string>
|
||||
<string name="server_lab_id">আইডি</string>
|
||||
<string name="server_lab_alterid">অলটারআইডি</string>
|
||||
<string name="server_lab_security">নিরাপত্তা</string>
|
||||
<string name="server_lab_network">নেটওয়ার্ক</string>
|
||||
<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">হোস্ট</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_stream_fingerprint" translatable="false">ফিঙ্গারপ্রিন্ট</string>
|
||||
<string name="server_lab_stream_alpn" translatable="false">Alpn</string>
|
||||
<string name="server_lab_allow_insecure">অনিরাপদ অনুমতি দিন</string>
|
||||
<string name="server_lab_sni">SNI</string>
|
||||
<string name="server_lab_address3">ঠিকানা</string>
|
||||
<string name="server_lab_port3">পোর্ট</string>
|
||||
<string name="server_lab_id3">পাসওয়ার্ড</string>
|
||||
<string name="server_lab_security3">নিরাপত্তা</string>
|
||||
<string name="server_lab_id4">পাসওয়ার্ড (ঐচ্ছিক)</string>
|
||||
<string name="server_lab_security4">ব্যবহারকারী (ঐচ্ছিক)</string>
|
||||
<string name="server_lab_encryption">এনক্রিপশন</string>
|
||||
<string name="server_lab_flow">ফ্লো</string>
|
||||
<string name="server_lab_public_key" translatable="false">পাবলিক কী</string>
|
||||
<string name="server_lab_short_id" translatable="false">শর্ট আইডি</string>
|
||||
<string name="server_lab_spider_x" translatable="false">SpiderX</string>
|
||||
<string name="server_lab_secret_key" translatable="false">সিক্রেট কী</string>
|
||||
<string name="server_lab_reserved">সংরক্ষিত (ঐচ্ছিক)</string>
|
||||
<string name="server_lab_local_address">স্থানীয় ঠিকানা (ঐচ্ছিক IPv4/IPv6, কমা দ্বারা পৃথক করা)</string>
|
||||
<string name="server_lab_local_mtu">MTU (ঐচ্ছিক, ডিফল্ট 1420)</string>
|
||||
<string name="toast_success">সফল</string>
|
||||
<string name="toast_failure">ব্যর্থ</string>
|
||||
<string name="toast_none_data">কোনও তথ্য নেই</string>
|
||||
<string name="toast_incorrect_protocol">ভুল প্রোটোকল</string>
|
||||
<string name="toast_decoding_failed">ডিকোডিং ব্যর্থ</string>
|
||||
<string name="title_file_chooser">একটি কনফিগারেশন ফাইল নির্বাচন করুন</string>
|
||||
<string name="toast_require_file_manager">অনুগ্রহ করে একটি ফাইল ম্যানেজার ইনস্টল করুন।</string>
|
||||
<string name="server_customize_config">কাস্টমাইজ কনফিগারেশন</string>
|
||||
<string name="toast_config_file_invalid">অবৈধ কনফিগারেশন</string>
|
||||
<string name="server_lab_content">কনটেন্ট</string>
|
||||
<string name="toast_none_data_clipboard">ক্লিপবোর্ডে কোনও তথ্য নেই</string>
|
||||
<string name="toast_invalid_url">অবৈধ URL</string>
|
||||
<string name="server_lab_need_inbound">ইনবাউন্ড পোর্ট নিশ্চিত করুন সেটিংসের সাথে সামঞ্জস্যপূর্ণ</string>
|
||||
<string name="toast_malformed_josn">কনফিগারেশন বিকৃত</string>
|
||||
<string name="server_lab_request_host6">হোস্ট (SNI) (ঐচ্ছিক)</string>
|
||||
<string name="toast_asset_copy_failed">ফাইল কপি ব্যর্থ, অনুগ্রহ করে ফাইল ম্যানেজার ব্যবহার করুন</string>
|
||||
<string name="menu_item_add_asset">অ্যাসেট যোগ করুন</string>
|
||||
<string name="menu_item_add_file">ফাইল যোগ করুন</string>
|
||||
<string name="menu_item_add_url">URL যোগ করুন</string>
|
||||
<string name="title_url" translatable="false">URL</string>
|
||||
<string name="menu_item_download_file">ফাইল ডাউনলোড করুন</string>
|
||||
<string name="title_user_asset_add_url">অ্যাসেট URL যোগ করুন</string>
|
||||
<string name="msg_file_not_found">ফাইল খুঁজে পাওয়া যায়নি</string>
|
||||
<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>
|
||||
<string name="menu_item_select_all">সবকিছু নির্বাচন করুন</string>
|
||||
<string name="msg_enter_keywords">কীওয়ার্ড লিখুন</string>
|
||||
<string name="switch_bypass_apps_mode">বাইপাস মোড</string>
|
||||
<string name="menu_item_select_proxy_app">অটো সিলেক্ট প্রক্সি অ্যাপ</string>
|
||||
<string name="msg_downloading_content">বিষয়বস্তু ডাউনলোড হচ্ছে</string>
|
||||
<string name="menu_item_export_proxy_app">ক্লিপবোর্ডে রপ্তানি করুন</string>
|
||||
<string name="menu_item_import_proxy_app">ক্লিপবোর্ড থেকে আমদানি করুন</string>
|
||||
|
||||
<!-- Preferences -->
|
||||
<string name="title_settings">সেটিংস</string>
|
||||
<string name="title_advanced">এডভান্সড সেটিংস</string>
|
||||
<string name="title_vpn_settings">VPN সেটিংস</string>
|
||||
<string name="title_pref_per_app_proxy">প্রতি-অ্যাপ প্রক্সি</string>
|
||||
<string name="summary_pref_per_app_proxy">সাধারণ: চেকড অ্যাপ প্রক্সি, আনচেকড সরাসরি সংযোগ; \nবাইপাস মোড: চেকড অ্যাপ সরাসরি সংযুক্ত, আনচেকড প্রক্সি। \nমেনুতে প্রক্সি অ্যাপ্লিকেশন স্বয়ংক্রিয়ভাবে নির্বাচন করার বিকল্প</string>
|
||||
|
||||
<string name="title_mux_settings">Mux সেটিংস</string>
|
||||
<string name="title_pref_mux_enabled">Mux সক্রিয় করুন</string>
|
||||
<string name="summary_pref_mux_enabled">দ্রুত, তবে এটি অস্থিতিশীল সংযোগ সৃষ্টি করতে পারে\nTCP, UDP এবং QUIC পরিচালনা কাস্টমাইজ করুন</string>
|
||||
<string name="title_pref_mux_concurency">TCP সংযোগ (পরিসর -1 থেকে 1024)</string>
|
||||
<string name="title_pref_mux_xudp_concurency">XUDP সংযোগ (পরিসর -1 থেকে 1024)</string>
|
||||
<string name="title_pref_mux_xudp_quic">Mux টানেলে QUIC পরিচালনা</string>
|
||||
<string-array name="mux_xudp_quic_entries">
|
||||
<item>অস্বীকার করুন</item>
|
||||
<item>অনুমতি দিন</item>
|
||||
<item>এড়িয়ে যান</item>
|
||||
</string-array>
|
||||
|
||||
<string name="title_pref_speed_enabled">গতি প্রদর্শন সক্রিয় করুন</string>
|
||||
<string name="summary_pref_speed_enabled">বিজ্ঞপ্তিতে বর্তমান গতি প্রদর্শন করুন।\nব্যবহারের উপর ভিত্তি করে বিজ্ঞপ্তি আইকন পরিবর্তিত হবে।</string>
|
||||
|
||||
<string name="title_pref_sniffing_enabled">স্নিফিং সক্রিয় করুন</string>
|
||||
<string name="summary_pref_sniffing_enabled">প্যাকেট থেকে ডোমেইন স্নিফ করার চেষ্টা করুন (ডিফল্টভাবে চালু)</string>
|
||||
|
||||
<string name="title_pref_route_only_enabled">রুটঅনলি সক্রিয় করুন</string>
|
||||
<string name="summary_pref_route_only_enabled">রুটিংয়ের জন্য শুধুমাত্র স্নিফড ডোমেইন নাম ব্যবহার করুন, এবং লক্ষ্য ঠিকানা হিসেবে আইপি ঠিকানা রাখুন।</string>
|
||||
|
||||
<string name="title_pref_local_dns_enabled">স্থানীয় DNS সক্রিয় করুন</string>
|
||||
<string name="summary_pref_local_dns_enabled">DNS মূল DNS মডিউল দ্বারা প্রক্রিয়া করা হয় (প্রস্তাবিত, যদি LAN এবং মূলভূমি ঠিকানার বাইপাসিং প্রয়োজন হয়)</string>
|
||||
|
||||
<string name="title_pref_fake_dns_enabled">ভুয়া DNS সক্রিয় করুন</string>
|
||||
<string name="summary_pref_fake_dns_enabled">স্থানীয় DNS মিথ্যা আইপি ঠিকানা ফেরত দেয় (দ্রুত, তবে এটি কিছু অ্যাপের জন্য কাজ নাও করতে পারে)</string>
|
||||
|
||||
<string name="title_pref_prefer_ipv6">IPv6 অগ্রাধিকার দিন</string>
|
||||
<string name="summary_pref_prefer_ipv6">IPv6 ঠিকানা এবং রুটকে অগ্রাধিকার দিন</string>
|
||||
|
||||
<string name="title_pref_routing">রাউটিং</string>
|
||||
<string name="title_pref_routing_domain_strategy">ডোমেইন কৌশল</string>
|
||||
<string name="title_pref_routing_mode">পূর্বনির্ধারিত নিয়ম</string>
|
||||
<string name="title_pref_routing_custom">কাস্টম নিয়ম</string>
|
||||
|
||||
<string name="title_pref_remote_dns">রিমোট DNS (udp/tcp/https/quic)(ঐচ্ছিক)</string>
|
||||
<string name="summary_pref_remote_dns">DNS</string>
|
||||
|
||||
<string name="title_pref_vpn_dns">VPN DNS (শুধুমাত্র IPv4/v6)</string>
|
||||
|
||||
<string name="title_pref_domestic_dns">ঘরোয়া DNS (ঐচ্ছিক)</string>
|
||||
<string name="summary_pref_domestic_dns">DNS</string>
|
||||
|
||||
<string name="title_pref_delay_test_url">সঠিক বিলম্ব পরীক্ষা ইউআরএল (http/https)</string>
|
||||
<string name="summary_pref_delay_test_url">ইউআরএল</string>
|
||||
|
||||
<string name="title_pref_proxy_sharing_enabled">LAN থেকে সংযোগ অনুমোদন করুন</string>
|
||||
<string name="summary_pref_proxy_sharing_enabled">অন্যান্য ডিভাইসগুলি আপনার আইপি ঠিকানা ব্যবহার করে প্রক্সিতে সংযুক্ত হতে পারে, শুধুমাত্র বিশ্বস্ত নেটওয়ার্কে সক্রিয় করুন যাতে অনুমোদিত সংযোগ এড়ানো যায়</string>
|
||||
<string name="toast_warning_pref_proxysharing_short">LAN থেকে সংযোগ অনুমোদন করুন, নিশ্চিত করুন যে আপনি একটি বিশ্বস্ত নেটওয়ার্কে আছেন</string>
|
||||
|
||||
<string name="title_pref_allow_insecure">allowInsecure</string>
|
||||
<string name="summary_pref_allow_insecure">যখন TLS, ডিফল্টভাবে allowInsecure</string>
|
||||
|
||||
<string name="title_pref_socks_port">SOCKS5 প্রক্সি পোর্ট</string>
|
||||
<string name="summary_pref_socks_port">SOCKS5 প্রক্সি পোর্ট</string>
|
||||
|
||||
<string name="title_pref_http_port">HTTP প্রক্সি পোর্ট</string>
|
||||
<string name="summary_pref_http_port">HTTP প্রক্সি পোর্ট</string>
|
||||
|
||||
<string name="title_pref_local_dns_port">স্থানীয় DNS পোর্ট</string>
|
||||
<string name="summary_pref_local_dns_port">স্থানীয় DNS পোর্ট</string>
|
||||
|
||||
<string name="title_pref_confirm_remove">কনফিগারেশন ফাইল মুছে ফেলার নিশ্চিতকরণ</string>
|
||||
<string name="summary_pref_confirm_remove">কনফিগারেশন ফাইল মুছে ফেলার জন্য ব্যবহারকারীর দ্বিতীয় নিশ্চিতকরণের প্রয়োজন</string>
|
||||
|
||||
<string name="title_pref_start_scan_immediate">তাত্ক্ষণিক স্ক্যান শুরু করুন</string>
|
||||
<string name="summary_pref_start_scan_immediate">শুরুতে তাত্ক্ষণিকভাবে স্ক্যান করতে ক্যামেরা খুলুন, অন্যথায় আপনি কোড স্ক্যান বা টুলবারে একটি ছবি নির্বাচন করতে পারেন</string>
|
||||
<string name="title_pref_feedback">মতামত</string>
|
||||
<string name="summary_pref_feedback">মতামত উন্নয়ন বা বাগগুলি GitHub-এ পাঠান</string>
|
||||
<string name="summary_pref_tg_group">টেলিগ্রাম গ্রুপে যোগদান করুন</string>
|
||||
<string name="toast_tg_app_not_found">টেলিগ্রাম অ্যাপ পাওয়া যায়নি</string>
|
||||
<string name="title_privacy_policy">গোপনীয়তা নীতি</string>
|
||||
<string name="title_about">সম্পর্কিত</string>
|
||||
<string name="title_source_code">সোর্স কোড</string>
|
||||
<string name="title_tg_channel">টেলিগ্রাম চ্যানেল</string>
|
||||
<string name="title_configuration_backup">কনফিগারেশন ব্যাকআপ</string>
|
||||
<string name="summary_configuration_backup">স্টোরেজ অবস্থান: [%s], অ্যাপ আনইনস্টল বা স্টোরেজ ক্লিয়ার করার পরে ব্যাকআপ মুছে যাবে</string>
|
||||
<string name="title_configuration_restore">কনফিগারেশন পুনরুদ্ধার</string>
|
||||
<string name="title_configuration_share">কনফিগারেশন শেয়ার করুন</string>
|
||||
|
||||
<string name="title_pref_promotion">প্রচার</string>
|
||||
<string name="summary_pref_promotion">প্রচার, বিস্তারিত জানার জন্য ক্লিক করুন (ডোনেশন মুছে ফেলা যেতে পারে)</string>
|
||||
|
||||
<string name="title_pref_auto_update_subscription">স্বয়ংক্রিয় আপডেট সাবস্ক্রিপশন</string>
|
||||
<string name="summary_pref_auto_update_subscription">পটভূমিতে একটি নির্দিষ্ট সময় পর পর আপনার সাবস্ক্রিপশন স্বয়ংক্রিয়ভাবে আপডেট করুন। ডিভাইসের উপর নির্ভর করে, এই বৈশিষ্ট্যটি সবসময় কাজ নাও করতে পারে</string>
|
||||
<string name="title_pref_auto_update_interval">অটো আপডেট ইন্টারভ্যাল (মিনিট, সর্বনিম্ন মান ১৫)</string>
|
||||
|
||||
<string name="title_core_loglevel">লগ স্তর</string>
|
||||
<string name="title_mode">মোড</string>
|
||||
<string name="title_mode_help">আরো সাহায্যের জন্য ক্লিক করুন</string>
|
||||
<string name="title_language">ভাষা</string>
|
||||
<string name="title_ui_settings">ইউআই সেটিংস</string>
|
||||
<string name="title_pref_ui_mode_night">ইউআই মোড সেটিংস</string>
|
||||
|
||||
<string name="title_logcat">লগক্যাট</string>
|
||||
<string name="logcat_copy">কপি করুন</string>
|
||||
<string name="logcat_clear">স্পষ্ট করুন</string>
|
||||
<string name="title_service_restart">সার্ভিস পুনরায় চালু করুন</string>
|
||||
<string name="title_del_all_config">সব কনফিগারেশন মুছে ফেলুন</string>
|
||||
<string name="title_del_duplicate_config">ডুপ্লিকেট কনফিগারেশন মুছে ফেলুন</string>
|
||||
<string name="title_del_invalid_config">অবৈধ কনফিগারেশন মুছে ফেলুন (প্রথমে পরীক্ষা করুন)</string>
|
||||
<string name="title_export_all">কাস্টম না করা কনফিগারেশনগুলি ক্লিপবোর্ডে রপ্তানি করুন</string>
|
||||
<string name="title_sub_setting">সাবস্ক্রিপশন গ্রুপ সেটিং</string>
|
||||
<string name="sub_setting_remarks">মন্তব্য</string>
|
||||
<string name="sub_setting_url">ঐচ্ছিক URL</string>
|
||||
<string name="sub_setting_enable">আপডেট সক্রিয় করুন</string>
|
||||
<string name="sub_auto_update">স্বয়ংক্রিয় আপডেট সক্রিয় করুন</string>
|
||||
<string name="title_sub_update">সাবস্ক্রিপশন আপডেট</string>
|
||||
<string name="title_ping_all_server">সব কনফিগারেশন TCPing</string>
|
||||
<string name="title_real_ping_all_server">সব কনফিগারেশন প্রকৃত বিলম্ব</string>
|
||||
<string name="title_user_asset_setting">জিও অ্যাসেট ফাইলগুলি</string>
|
||||
<string name="title_sort_by_test_results">টেস্ট ফলাফল দ্বারা সাজানো</string>
|
||||
<string name="title_filter_config">কনফিগারেশন ফাইল ফিল্টার করুন</string>
|
||||
<string name="filter_config_all">সব সাবস্ক্রিপশন গ্রুপ</string>
|
||||
<string name="title_del_duplicate_config_count">%d ডুপ্লিকেট কনফিগারেশন মুছে ফেলুন</string>
|
||||
<string name="tasker_start_service">সার্ভিস শুরু করুন</string>
|
||||
<string name="tasker_setting_confirm">নিশ্চিত করুন</string>
|
||||
|
||||
<string name="routing_settings_title">রাউটিং সেটিংস</string>
|
||||
<string name="routing_settings_tips">কমা (,) দ্বারা আলাদা করুন, মনে রাখবেন সেভ করতে</string>
|
||||
<string name="routing_settings_save">সেভ করুন</string>
|
||||
<string name="routing_settings_delete">মুছে ফেলুন</string>
|
||||
<string name="routing_settings_scan_replace">স্ক্যান করুন এবং প্রতিস্থাপন করুন</string>
|
||||
<string name="routing_settings_scan_append">স্ক্যান করুন এবং যোগ করুন</string>
|
||||
<string name="routing_settings_default_rules"> ডিফল্ট রাউটিং নিয়ম সেট করুন</string>
|
||||
|
||||
<string name="connection_test_pending">সংযোগ পরীক্ষা করুন</string>
|
||||
<string name="connection_test_testing">পরীক্ষা চলছে…</string>
|
||||
<string name="connection_test_available">সফল: HTTP সংযোগ নিয়েছে %dms</string>
|
||||
<string name="connection_test_error">ইন্টারনেট সংযোগ সনাক্ত করতে ব্যর্থ: %s</string>
|
||||
<string name="connection_test_fail">ইন্টারনেট উপলব্ধ নয়</string>
|
||||
<string name="connection_test_error_status_code">ত্রুটি কোড: #%d</string>
|
||||
<string name="connection_connected">সংযুক্ত, সংযোগ পরীক্ষা করতে ট্যাপ করুন</string>
|
||||
<string name="connection_not_connected">সংযুক্ত নয়</string>
|
||||
|
||||
<string name="import_subscription_success">সাবস্ক্রিপশন সফলভাবে আমদানি করা হয়েছে</string>
|
||||
<string name="import_subscription_failure">সাবস্ক্রিপশন আমদানি ব্যর্থ</string>
|
||||
<string name="title_fragment_settings">ফ্র্যাগমেন্ট সেটিংস</string>
|
||||
<string name="title_pref_fragment_packets">ফ্র্যাগমেন্ট প্যাকেটস</string>
|
||||
<string name="title_pref_fragment_length">ফ্র্যাগমেন্ট দৈর্ঘ্য (ন্যূনতম-সর্বাধিক)</string>
|
||||
<string name="title_pref_fragment_interval">ফ্র্যাগমেন্ট ইন্টারভ্যাল (ন্যূনতম-সর্বাধিক)</string>
|
||||
<string name="title_pref_fragment_enabled">ফ্র্যাগমেন্ট সক্রিয় করুন</string>
|
||||
<string-array name="share_method">
|
||||
<item>QR কোড</item>
|
||||
<item>ক্লিপবোর্ডে রপ্তানি করুন</item>
|
||||
<item>পূর্ণ কনফিগারেশন ক্লিপবোর্ডে রপ্তানি করুন</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="share_sub_method">
|
||||
<item>QR কোড</item>
|
||||
<item>ক্লিপবোর্ডে রপ্তানি করুন</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="routing_tag">
|
||||
<item>প্রক্সি URL বা IP</item>
|
||||
<item>ডাইরেক্ট URL বা IP</item>
|
||||
<item>ব্লকড URL বা IP</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="routing_mode">
|
||||
<item>গ্লোবাল প্রোক্সি</item>
|
||||
<item>LAN ঠিকানা বাইপাস করে তারপর প্রোক্সি</item>
|
||||
<item>মেইনল্যান্ড ঠিকানা বাইপাস করে তারপর প্রোক্সি</item>
|
||||
<item>LAN এবং মেইনল্যান্ড ঠিকানা বাইপাস করে তারপর প্রোক্সি</item>
|
||||
<item>গ্লোবাল ডাইরেক্ট</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="mode_entries">
|
||||
<item>VPN</item>
|
||||
<item>শুধুমাত্র প্রোক্সি</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="ui_mode_night">
|
||||
<item>সিস্টেম অনুসরণ করুন</item>
|
||||
<item>লাইট</item>
|
||||
<item>ডার্ক</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
@@ -36,6 +36,7 @@
|
||||
<string name="menu_item_import_config_custom_url">کانفیگ سفارشی را از طریق نشانی اینترنتی وارد کنید</string>
|
||||
<string name="menu_item_import_config_custom_url_scan">نشانی اینترنتی اسکن کانفیگ سفارشی را وارد کنید</string>
|
||||
<string name="del_config_comfirm">حذف شود؟</string>
|
||||
<string name="del_invalid_config_comfirm">Please test before deleting! Confirm delete ?</string>
|
||||
<string name="server_lab_remarks">ملاحظات</string>
|
||||
<string name="server_lab_address">نشانی</string>
|
||||
<string name="server_lab_port">پورت</string>
|
||||
@@ -46,9 +47,25 @@
|
||||
<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_stream_fingerprint">اثرانگشت</string>
|
||||
<string name="server_lab_stream_alpn">Alpn</string>
|
||||
<string name="server_lab_allow_insecure">مجوز ناامن</string>
|
||||
<string name="server_lab_sni">SNI</string>
|
||||
<string name="server_lab_address3">نشانی</string>
|
||||
@@ -59,10 +76,14 @@
|
||||
<string name="server_lab_security4">نامکاربری (اختیاری)</string>
|
||||
<string name="server_lab_encryption">رمزنگاری</string>
|
||||
<string name="server_lab_flow">جریان</string>
|
||||
<string name="server_lab_public_key">PublicKey</string>
|
||||
<string name="server_lab_short_id">ShortId</string>
|
||||
<string name="server_lab_spider_x">SpiderX</string>
|
||||
<string name="server_lab_secret_key">SecretKey</string>
|
||||
<string name="server_lab_reserved">Reserved (اختیاری)</string>
|
||||
<string name="server_lab_local_address">آدرس محلی IPv4(اختیاری)</string>
|
||||
<string name="server_lab_local_mtu">Mtu(optional, default 1420)</string>
|
||||
<string name="toast_success">موفقیت</string>
|
||||
<string name="toast_success">با موفقیت انجام شد</string>
|
||||
<string name="toast_failure">شکست</string>
|
||||
<string name="toast_none_data">چیزی نیست</string>
|
||||
<string name="toast_incorrect_protocol">پروتکل نادرست</string>
|
||||
@@ -79,6 +100,7 @@
|
||||
<string name="server_lab_request_host6">میزبان (SNI) (اختیاری)</string>
|
||||
<string name="toast_asset_copy_failed">کپی فایل انجام نشد، لطفا از برنامه مدیریت فایل استفاده کنید</string>
|
||||
<string name="menu_item_add_file">افزودن فایلها</string>
|
||||
<string name="title_url">URL</string>
|
||||
<string name="menu_item_download_file">دانلود فایلها</string>
|
||||
<string name="toast_action_not_allowed">این عمل ممنوع است</string>
|
||||
|
||||
@@ -177,7 +199,7 @@
|
||||
<string name="toast_tg_app_not_found">برنامه تلگرام پیدا نشد</string>
|
||||
|
||||
<string name="title_privacy_policy">حریم خصوصی</string>
|
||||
<string name="title_about">About</string>
|
||||
<string name="title_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>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_widget_name">Переключить</string>
|
||||
<string name="app_tile_name">Переключить</string>
|
||||
<string name="app_widget_name">v2rayNG</string>
|
||||
<string name="app_tile_name">v2rayNG</string>
|
||||
<string name="app_tile_first_use">Первое использование этой функции, пожалуйста, используйте приложение, чтобы добавить сервер</string>
|
||||
<string name="navigation_drawer_open">Открыть панель навигации</string>
|
||||
<string name="navigation_drawer_close">Закрыть панель навигации</string>
|
||||
@@ -36,6 +36,7 @@
|
||||
<string name="menu_item_import_config_custom_url">Импорт из URL</string>
|
||||
<string name="menu_item_import_config_custom_url_scan">Импорт сканированием URL</string>
|
||||
<string name="del_config_comfirm">Подтверждаете удаление?</string>
|
||||
<string name="del_invalid_config_comfirm">Выполните проверку перед удалением! Подтверждаете удаление?</string>
|
||||
<string name="server_lab_remarks">Описание</string>
|
||||
<string name="server_lab_address">Адрес</string>
|
||||
<string name="server_lab_port">Порт</string>
|
||||
@@ -46,9 +47,25 @@
|
||||
<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_stream_fingerprint">Отпечаток</string>
|
||||
<string name="server_lab_stream_alpn">ALPN</string>
|
||||
<string name="server_lab_allow_insecure">Разрешать небезопасные</string>
|
||||
<string name="server_lab_sni">SNI</string>
|
||||
<string name="server_lab_address3">Адрес</string>
|
||||
@@ -59,6 +76,10 @@
|
||||
<string name="server_lab_security4">Пользователь (необязательно)</string>
|
||||
<string name="server_lab_encryption">Шифрование</string>
|
||||
<string name="server_lab_flow">Поток</string>
|
||||
<string name="server_lab_public_key">PublicKey</string>
|
||||
<string name="server_lab_short_id">ShortID</string>
|
||||
<string name="server_lab_spider_x">SpiderX</string>
|
||||
<string name="server_lab_secret_key">SecretKey</string>
|
||||
<string name="server_lab_reserved">Reserved (необязательно)</string>
|
||||
<string name="server_lab_local_address">Локальный адрес (необязательно, IPv4/IPv6 через запятую)</string>
|
||||
<string name="server_lab_local_mtu">MTU (необязательно, по умолчанию 1420)</string>
|
||||
@@ -81,12 +102,14 @@
|
||||
<string name="menu_item_add_asset">Добавить ресурс</string>
|
||||
<string name="menu_item_add_file">Добавить файлы</string>
|
||||
<string name="menu_item_add_url">Добавить URL</string>
|
||||
<string name="title_url">URL</string>
|
||||
<string name="menu_item_download_file">Загрузить файлы</string>
|
||||
<string name="title_user_asset_add_url">Добавить URL ресурса</string>
|
||||
<string name="msg_file_not_found">Файл не найден</string>
|
||||
<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 +149,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 +171,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 +200,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>
|
||||
@@ -205,18 +227,18 @@
|
||||
<string name="logcat_copy">Копировать</string>
|
||||
<string name="logcat_clear">Очистить</string>
|
||||
<string name="title_service_restart">Перезапуск службы</string>
|
||||
<string name="title_del_all_config">Удалить все профили</string>
|
||||
<string name="title_del_duplicate_config">Удалить дубликаты профилей</string>
|
||||
<string name="title_del_invalid_config">Удалить нерабочие профили (после проверки)</string>
|
||||
<string name="title_export_all">Экспорт всех профилей в буфер обмена</string>
|
||||
<string name="title_del_all_config">Удалить профили группы</string>
|
||||
<string name="title_del_duplicate_config">Удалить дубликаты профилей группы</string>
|
||||
<string name="title_del_invalid_config">Удалить нерабочие профили группы</string>
|
||||
<string name="title_export_all">Экспорт профилей группы в буфер обмена</string>
|
||||
<string name="title_sub_setting">Группы</string>
|
||||
<string name="sub_setting_remarks">Название</string>
|
||||
<string name="sub_setting_url">URL (необязательно)</string>
|
||||
<string name="sub_setting_enable">Использовать обновление</string>
|
||||
<string name="sub_auto_update">Использовать автоматическое обновление</string>
|
||||
<string name="title_sub_update">Обновить подписку</string>
|
||||
<string name="title_ping_all_server">Проверка доступности профилей</string>
|
||||
<string name="title_real_ping_all_server">Время отклика профилей</string>
|
||||
<string name="title_sub_update">Обновить подписку группы</string>
|
||||
<string name="title_ping_all_server">Проверка профилей группы</string>
|
||||
<string name="title_real_ping_all_server">Время отклика профилей группы</string>
|
||||
<string name="title_user_asset_setting">Файлы георесурсов</string>
|
||||
<string name="title_sort_by_test_results">Сортировка по результатам теста</string>
|
||||
<string name="title_filter_config">Фильтр групп</string>
|
||||
|
||||
@@ -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,49 +24,70 @@
|
||||
<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>
|
||||
<string name="menu_item_import_config_custom_url">Nhập cấu hình tùy chỉnh từ URL</string>
|
||||
<string name="menu_item_import_config_custom_url_scan">Nhập cấu hình tùy chỉnh quét URL</string>
|
||||
<string name="del_config_comfirm">Xác nhận xóa?</string>
|
||||
<string name="del_invalid_config_comfirm">Please test before deleting! Confirm delete ?</string>
|
||||
<string name="server_lab_remarks">Tên cấu hình</string>
|
||||
<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_stream_fingerprint">Fingerprint</string>
|
||||
<string name="server_lab_stream_alpn">Alpn</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_flow">Kiểm soát lưu lượng (Flow)</string>
|
||||
<string name="server_lab_public_key">PublicKey</string>
|
||||
<string name="server_lab_short_id">ShortId</string>
|
||||
<string name="server_lab_spider_x">SpiderX</string>
|
||||
<string name="server_lab_secret_key">SecretKey</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_local_mtu">MTU (Không bắt buộc, mặc định là 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 +97,17 @@
|
||||
<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="title_url">URL</string>
|
||||
<string name="menu_item_download_file">Tải xuống tệp tin</string>
|
||||
<string name="toast_action_not_allowed">Hành động này bị cấm</string>
|
||||
<string name="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 +115,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 +123,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 +139,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_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="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 +198,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 là 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 +229,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 +237,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 +248,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 +269,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 +300,14 @@
|
||||
<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>
|
||||
<string name="title_fragment_settings">Fragment Settings</string>
|
||||
<string name="title_pref_fragment_packets">Fragment Packets</string>
|
||||
<string name="title_pref_fragment_length">Fragment Length (min-max)</string>
|
||||
<string name="title_pref_fragment_interval">Fragment Interval (min-max)</string>
|
||||
<string name="title_pref_fragment_enabled">Enable Fragment</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
<string name="menu_item_import_config_custom_url">剪贴板URL导入自定义配置</string>
|
||||
<string name="menu_item_import_config_custom_url_scan">扫描URL导入自定义配置</string>
|
||||
<string name="del_config_comfirm">确认删除?</string>
|
||||
<string name="del_invalid_config_comfirm">删除前请先测试!确认删除?</string>
|
||||
<string name="server_lab_remarks">别名(remarks)</string>
|
||||
<string name="server_lab_address">地址(address)</string>
|
||||
<string name="server_lab_port">端口(port)</string>
|
||||
@@ -46,9 +47,25 @@
|
||||
<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_stream_fingerprint">Fingerprint</string>
|
||||
<string name="server_lab_stream_alpn">Alpn</string>
|
||||
<string name="server_lab_allow_insecure">跳过证书验证(allowInsecure)</string>
|
||||
<string name="server_lab_sni">SNI</string>
|
||||
<string name="server_lab_address3">服务器地址</string>
|
||||
@@ -59,6 +76,10 @@
|
||||
<string name="server_lab_security4">用户名(可选)</string>
|
||||
<string name="server_lab_encryption">加密方式(encryption)</string>
|
||||
<string name="server_lab_flow">流控(flow)</string>
|
||||
<string name="server_lab_public_key">PublicKey</string>
|
||||
<string name="server_lab_short_id">ShortId</string>
|
||||
<string name="server_lab_spider_x">SpiderX</string>
|
||||
<string name="server_lab_secret_key">SecretKey</string>
|
||||
<string name="server_lab_reserved">Reserved(可选)</string>
|
||||
<string name="server_lab_local_address">本地地址(可选IPv4/IPv6,逗号隔开)</string>
|
||||
<string name="server_lab_local_mtu">Mtu(可选, 默认1420)</string>
|
||||
@@ -79,6 +100,7 @@
|
||||
<string name="server_lab_request_host6">Host(SNI)(可选)</string>
|
||||
<string name="toast_asset_copy_failed">失败, 请使用文件管理器</string>
|
||||
<string name="menu_item_add_file">添加文件</string>
|
||||
<string name="title_url">URL</string>
|
||||
<string name="menu_item_download_file">下载文件</string>
|
||||
<string name="toast_action_not_allowed">禁止此项操作</string>
|
||||
|
||||
@@ -202,22 +224,22 @@
|
||||
<string name="logcat_copy">复制</string>
|
||||
<string name="logcat_clear">清除</string>
|
||||
<string name="title_service_restart">服务重启</string>
|
||||
<string name="title_del_all_config">删除全部配置</string>
|
||||
<string name="title_del_duplicate_config">删除重复配置</string>
|
||||
<string name="title_del_invalid_config">删除无效配置(先测试)</string>
|
||||
<string name="title_export_all">导出全部(非自定义)配置至剪贴板</string>
|
||||
<string name="title_del_all_config">删除当前组配置</string>
|
||||
<string name="title_del_duplicate_config">删除当前组重复配置</string>
|
||||
<string name="title_del_invalid_config">删除当前组无效配置</string>
|
||||
<string name="title_export_all">导出当前组配置至剪贴板</string>
|
||||
<string name="title_sub_setting">订阅分组设置</string>
|
||||
<string name="sub_setting_remarks">备注</string>
|
||||
<string name="sub_setting_url">可选地址(url)</string>
|
||||
<string name="sub_setting_enable">启用更新</string>
|
||||
<string name="sub_auto_update">启用自动更新</string>
|
||||
<string name="title_sub_update">更新订阅</string>
|
||||
<string name="title_ping_all_server">测试全部配置Tcping</string>
|
||||
<string name="title_real_ping_all_server">测试全部配置真连接</string>
|
||||
<string name="title_sub_update">更新当前组订阅</string>
|
||||
<string name="title_ping_all_server">测试当前组配置Tcping</string>
|
||||
<string name="title_real_ping_all_server">测试当前组配置真连接</string>
|
||||
<string name="title_user_asset_setting">Geo 资源文件</string>
|
||||
<string name="title_sort_by_test_results">按测试结果排序</string>
|
||||
<string name="title_filter_config">过滤配置文件</string>
|
||||
<string name="filter_config_all">所有订阅分组</string>
|
||||
<string name="filter_config_all">所有分组</string>
|
||||
<string name="title_del_duplicate_config_count">删除 %d 个重复配置</string>
|
||||
|
||||
<string name="tasker_start_service">启动服务</string>
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
<string name="menu_item_import_config_custom_url">從 URL 匯入自訂配置</string>
|
||||
<string name="menu_item_import_config_custom_url_scan">掃描 URL 匯入自訂配置</string>
|
||||
<string name="del_config_comfirm">確定刪除?</string>
|
||||
<string name="del_invalid_config_comfirm">刪除前請先測試!確認刪除?</string>
|
||||
<string name="server_lab_remarks">備註</string>
|
||||
<string name="server_lab_address">位址</string>
|
||||
<string name="server_lab_port">埠</string>
|
||||
@@ -46,9 +47,25 @@
|
||||
<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_stream_fingerprint">Fingerprint</string>
|
||||
<string name="server_lab_stream_alpn">Alpn</string>
|
||||
<string name="server_lab_allow_insecure">跳過憑證驗證 (allowInsecure)</string>
|
||||
<string name="server_lab_sni">SNI</string>
|
||||
<string name="server_lab_address3">伺服器位址</string>
|
||||
@@ -59,6 +76,10 @@
|
||||
<string name="server_lab_security4">使用者名稱 (可選)</string>
|
||||
<string name="server_lab_encryption">加密 (encryption)</string>
|
||||
<string name="server_lab_flow">流程 (flow)</string>
|
||||
<string name="server_lab_public_key">PublicKey</string>
|
||||
<string name="server_lab_short_id">ShortId</string>
|
||||
<string name="server_lab_spider_x">SpiderX</string>
|
||||
<string name="server_lab_secret_key">SecretKey</string>
|
||||
<string name="server_lab_reserved">Reserved (可選)</string>
|
||||
<string name="server_lab_local_address">本機位址(可選IPv4/IPv6,逗號隔開)</string>
|
||||
<string name="server_lab_local_mtu">MTU(可選, 預設1420)</string>
|
||||
@@ -79,6 +100,7 @@
|
||||
<string name="server_lab_request_host6">Host(SNI)(可選)</string>
|
||||
<string name="toast_asset_copy_failed">失敗,請使用檔案總管</string>
|
||||
<string name="menu_item_add_file">新增檔案</string>
|
||||
<string name="title_url">URL</string>
|
||||
<string name="menu_item_download_file">下載檔案</string>
|
||||
<string name="toast_action_not_allowed">禁止此項操作</string>
|
||||
|
||||
@@ -203,22 +225,22 @@
|
||||
<string name="logcat_copy">複製</string>
|
||||
<string name="logcat_clear">清除</string>
|
||||
<string name="title_service_restart">重啟服務</string>
|
||||
<string name="title_del_all_config">刪除全部配置</string>
|
||||
<string name="title_del_duplicate_config">刪除重複配置</string>
|
||||
<string name="title_del_invalid_config">刪除無效配置 (先偵測)</string>
|
||||
<string name="title_export_all">匯出全部 (非自訂) 配置至剪貼簿</string>
|
||||
<string name="title_del_all_config">刪除目前群組配置</string>
|
||||
<string name="title_del_duplicate_config">刪除目前群組重複配置</string>
|
||||
<string name="title_del_invalid_config">刪除目前群組無效配置</string>
|
||||
<string name="title_export_all">匯出目前群組配置至剪貼簿</string>
|
||||
<string name="title_sub_setting">訂閱分組設定</string>
|
||||
<string name="sub_setting_remarks">備註</string>
|
||||
<string name="sub_setting_url">Optional URL</string>
|
||||
<string name="sub_setting_enable">啟用更新</string>
|
||||
<string name="sub_auto_update">啟用自動更新</string>
|
||||
<string name="title_sub_update">更新訂閱</string>
|
||||
<string name="title_ping_all_server">偵測所有配置 Tcping</string>
|
||||
<string name="title_real_ping_all_server">偵測所有配置真延遲</string>
|
||||
<string name="title_sub_update">更新目前群組訂閱</string>
|
||||
<string name="title_ping_all_server">偵測目前群組配置 Tcping</string>
|
||||
<string name="title_real_ping_all_server">偵測目前群組配置真延遲</string>
|
||||
<string name="title_user_asset_setting">Geo 資源檔案</string>
|
||||
<string name="title_sort_by_test_results">依偵測結果排序</string>
|
||||
<string name="title_filter_config">過濾配置</string>
|
||||
<string name="filter_config_all">所有訂閱分組</string>
|
||||
<string name="filter_config_all">所有分組</string>
|
||||
<string name="title_del_duplicate_config_count">Delete %d duplicate configurations</string>
|
||||
|
||||
<string name="tasker_start_service">啟動服務</string>
|
||||
|
||||
@@ -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>
|
||||
@@ -177,6 +178,7 @@
|
||||
<item>Русский</item>
|
||||
<item>فارسی</item>
|
||||
<item>عربي</item>
|
||||
<item>বাংলা</item>
|
||||
</string-array>
|
||||
|
||||
|
||||
@@ -189,6 +191,7 @@
|
||||
<item>ru</item>
|
||||
<item>fa</item>
|
||||
<item>ar</item>
|
||||
<item>bn</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="mux_xudp_quic_value" translatable="false">
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="TabLayoutTextStyle" parent="TextAppearance.Design.Tab">
|
||||
<item name="textAllCaps">false</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -36,7 +36,8 @@
|
||||
<string name="menu_item_import_config_custom_local">Import custom config from locally</string>
|
||||
<string name="menu_item_import_config_custom_url">Import custom config from URL</string>
|
||||
<string name="menu_item_import_config_custom_url_scan">Import custom config scan URL</string>
|
||||
<string name="del_config_comfirm">Confirm delete?</string>
|
||||
<string name="del_config_comfirm">Confirm delete ?</string>
|
||||
<string name="del_invalid_config_comfirm">Please test before deleting! Confirm delete ?</string>
|
||||
<string name="server_lab_remarks">remarks</string>
|
||||
<string name="server_lab_address">address</string>
|
||||
<string name="server_lab_port">port</string>
|
||||
@@ -47,11 +48,25 @@
|
||||
<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>
|
||||
<string name="server_lab_stream_alpn">Alpn</string>
|
||||
<string name="server_lab_allow_insecure">allowInsecure</string>
|
||||
<string name="server_lab_sni">SNI</string>
|
||||
<string name="server_lab_address3">address</string>
|
||||
@@ -62,10 +77,10 @@
|
||||
<string name="server_lab_security4">User(Optional)</string>
|
||||
<string name="server_lab_encryption">encryption</string>
|
||||
<string name="server_lab_flow">flow</string>
|
||||
<string name="server_lab_public_key" translatable="false">PublicKey</string>
|
||||
<string name="server_lab_short_id" translatable="false">ShortId</string>
|
||||
<string name="server_lab_spider_x" translatable="false">SpiderX</string>
|
||||
<string name="server_lab_secret_key" translatable="false">SecretKey</string>
|
||||
<string name="server_lab_public_key">PublicKey</string>
|
||||
<string name="server_lab_short_id">ShortId</string>
|
||||
<string name="server_lab_spider_x">SpiderX</string>
|
||||
<string name="server_lab_secret_key">SecretKey</string>
|
||||
<string name="server_lab_reserved">Reserved(Optional)</string>
|
||||
<string name="server_lab_local_address">Local address (optional IPv4/IPv6, separated by commas)</string>
|
||||
<string name="server_lab_local_mtu">Mtu(optional, default 1420)</string>
|
||||
@@ -88,7 +103,7 @@
|
||||
<string name="menu_item_add_asset">Add asset</string>
|
||||
<string name="menu_item_add_file">Add files</string>
|
||||
<string name="menu_item_add_url">Add URL</string>
|
||||
<string name="title_url" translatable="false">URL</string>
|
||||
<string name="title_url">URL</string>
|
||||
<string name="menu_item_download_file">Download files</string>
|
||||
<string name="title_user_asset_add_url">Add asset URL</string>
|
||||
<string name="msg_file_not_found">File not found</string>
|
||||
@@ -215,22 +230,22 @@
|
||||
<string name="logcat_copy">Copy</string>
|
||||
<string name="logcat_clear">Clear</string>
|
||||
<string name="title_service_restart">Service restart</string>
|
||||
<string name="title_del_all_config">Delete all config</string>
|
||||
<string name="title_del_duplicate_config">Delete duplicate config</string>
|
||||
<string name="title_del_invalid_config">Delete invalid config(Test first)</string>
|
||||
<string name="title_export_all">Export non-custom configs to clipboard</string>
|
||||
<string name="title_del_all_config">Delete current group configuration</string>
|
||||
<string name="title_del_duplicate_config">Delete current group duplicate configuration</string>
|
||||
<string name="title_del_invalid_config">Delete current group invalid configuration</string>
|
||||
<string name="title_export_all">Export current group non-custom configs to clipboard</string>
|
||||
<string name="title_sub_setting">Subscription group setting</string>
|
||||
<string name="sub_setting_remarks">remarks</string>
|
||||
<string name="sub_setting_url">Optional URL</string>
|
||||
<string name="sub_setting_enable">Enable update</string>
|
||||
<string name="sub_auto_update">Enable automatic update</string>
|
||||
<string name="title_sub_update">Update subscription</string>
|
||||
<string name="title_ping_all_server">Tcping all configuration</string>
|
||||
<string name="title_real_ping_all_server">Real delay all configuration</string>
|
||||
<string name="title_sub_update">Update current group subscription</string>
|
||||
<string name="title_ping_all_server">Tcping current group configuration</string>
|
||||
<string name="title_real_ping_all_server">Real delay current group configuration</string>
|
||||
<string name="title_user_asset_setting">Geo asset files</string>
|
||||
<string name="title_sort_by_test_results">Sorting by test results</string>
|
||||
<string name="title_filter_config">Filter configuration file</string>
|
||||
<string name="filter_config_all">All subscription groups</string>
|
||||
<string name="filter_config_all">All groups</string>
|
||||
<string name="title_del_duplicate_config_count">Delete %d duplicate configurations</string>
|
||||
|
||||
<string name="tasker_start_service">Start Service</string>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<cache-path name="cache" path="/"/>
|
||||
<cache-path
|
||||
name="cache"
|
||||
path="/" />
|
||||
</paths>
|
||||
|
||||
@@ -109,13 +109,13 @@
|
||||
|
||||
<EditTextPreference
|
||||
android:inputType="number"
|
||||
android:key="pref_mux_concurency"
|
||||
android:key="pref_mux_concurrency"
|
||||
android:summary="8"
|
||||
android:title="@string/title_pref_mux_concurency" />
|
||||
|
||||
<EditTextPreference
|
||||
android:inputType="number"
|
||||
android:key="pref_mux_xudp_concurency"
|
||||
android:key="pref_mux_xudp_concurrency"
|
||||
android:summary="8"
|
||||
android:title="@string/title_pref_mux_xudp_concurency" />
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.v2ray.ang;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* To work on unit tests, switch the Test Artifact in the Build Variants view.
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
import com.v2ray.ang.util.Utils
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class UtilTest {
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user