Compare commits

...

23 Commits

Author SHA1 Message Date
2dust
1e7f49b756 up 1.9.44 2025-03-30 19:14:55 +08:00
2dust
ac4c0f7ee1 Optimize and improve Log 2025-03-30 19:05:35 +08:00
2dust
6cc91b1a89 Optimize and improve log
Use Log.e() instead of e.printStackTrace()
2025-03-30 17:40:36 +08:00
2dust
45facff41d Optimize and improve Utils 2025-03-30 16:28:14 +08:00
2dust
ee703e6c95 Remove ads rules from default routing rules 2025-03-30 11:18:39 +08:00
hhhkkmk
87213c34a6 Revert "Optimization (#4426)" (#4437)
This reverts commit d111328541.
2025-03-29 18:06:09 +08:00
solokot
73a7c76183 Update Russian translation (#4435) 2025-03-29 18:05:43 +08:00
Pk-web6936
ed5282f2b3 Update dependencies (#4432)
* Update libs.versions.toml

Update agp

* Update validate-fastlane-supply-metadata

Update validate-fastlane-supply-metadata
2025-03-29 10:47:09 +08:00
Pk-web6936
390c657047 Switch to Loyalsoldier's v2ray-rules-dat (#4431)
https://github.com/2dust/AndroidLibXrayLite/pull/132/files
2025-03-29 10:46:49 +08:00
Pk-web6936
7071072862 Fix Arabic Language (#4430)
* Fix Arabic Language

* Fix Arabic Language

Fix Arabic Language
2025-03-29 10:46:29 +08:00
Pk-web6936
d111328541 Optimization (#4426)
* Optimization

Add security flags to CFLAGS and LDFLAGS. Use local variables instead of global variables. Clean up and simplify the script.

* Optimizition

Add security flags to CFLAGS and LDFLAGS. Simplify and organize the file. Makefile

* Optimization

Add security flags to CFLAGS and LDFLAGS. Use local variables instead of global variables. Clean up and simplify the script.

* Optimization
2025-03-29 10:44:21 +08:00
Pk-web6936
76cb2aaf46 Update Persian translate (#4424) 2025-03-29 10:44:03 +08:00
2dust
7ff1397163 Optimize the test true delay function
When testing, remove unnecessary configurations such as dns and routing to reduce resource usage, except for custom configurations.
2025-03-29 10:43:25 +08:00
2dust
bcf5d49a3d up 1.9.43 2025-03-28 16:20:12 +08:00
2dust
4fffb17283 Added user asset file settings in the drawer menu 2025-03-28 15:08:52 +08:00
2dust
83b8bdfdf4 Optimize UI 2025-03-28 15:07:24 +08:00
2dust
cc1538a24d Code clean 2025-03-28 11:02:45 +08:00
2dust
eb19199d18 Update .gitignore 2025-03-28 10:56:59 +08:00
Pk-web6936
441e5ef8d5 Consolidate and Optimize .gitignore Files (#4421)
* Delete .gitignore

* Delete V2rayNG/.gitignore

* Delete V2rayNG/app/.gitignore

* Create .gitignore

* Add New .gitignore
2025-03-28 09:24:22 +08:00
2dust
d768774aad Optimize and improve toast
Migrate from https://github.com/PureWriter/ToastCompat to https://github.com/GrenderG/Toasty
2025-03-27 17:43:58 +08:00
2dust
c3d83907a5 Optimize and improve 2025-03-27 13:46:35 +08:00
solokot
b52a98ae5e Update Russian translation (#4416) 2025-03-27 09:12:52 +08:00
Pk-web6936
a70e4089e3 Update Persian translate (#4415) 2025-03-27 09:12:41 +08:00
108 changed files with 890 additions and 588 deletions

View File

@@ -13,4 +13,4 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Validate Fastlane Supply Metadata
uses: ashutoshgngwr/validate-fastlane-supply-metadata@v2.0.0
uses: ashutoshgngwr/validate-fastlane-supply-metadata@v2.1.0

60
.gitignore vendored
View File

@@ -1,6 +1,66 @@
# Ignore data and key store files
*.dat
*.jks
# Ignore output JSON file
V2rayNG/app/release/output.json
# Ignore IDE and build system directories
.idea/
.gradle/
*.iml
# Ignore local properties and DS_Store files
/local.properties
.DS_Store
# Ignore build directories and captures
/build
/captures
V2rayNG/app/build
V2rayNG/build
V2rayNG/local.properties
# Ignore APK and AAR files
*.apk
*.aar
# Ignore signing properties
signing.properties
# Ignore shared object files
*.so
# Ignore Google services JSON
V2rayNG/app/google-services.json
# Additional common Android/Java ignores
*.log
*.tmp
*.bak
*.swp
*.orig
*.class
*.jar
*.war
*.ear
# Ignore executable files
*.exe
*.dll
*.obj
*.o
*.pyc
*.pyo
# Ignore files from other IDEs
.vscode/
.classpath
.project
.settings/
*.sublime-workspace
*.sublime-project
# Ignore OS-specific files
Thumbs.db
.DS_Store

View File

@@ -21,7 +21,7 @@ A V2Ray client for Android, support [Xray core](https://github.com/XTLS/Xray-cor
#### Geoip and Geosite
- geoip.dat and geosite.dat files are in `Android/data/com.v2ray.ang/files/assets` (path may differ on some Android device)
- download feature will get enhanced version in this [repo](https://github.com/Loyalsoldier/v2ray-rules-dat) (Note it need a working proxy)
- latest official [domain list](https://github.com/v2fly/domain-list-community) and [ip list](https://github.com/v2fly/geoip) can be imported manually
- latest official [domain list](https://github.com/Loyalsoldier/v2ray-rules-dat) and [ip list](https://github.com/Loyalsoldier/geoip) can be imported manually
- possible to use third party dat file in the same folder, like [h2y](https://guide.v2fly.org/routing/sitedata.html#%E5%A4%96%E7%BD%AE%E7%9A%84%E5%9F%9F%E5%90%8D%E6%96%87%E4%BB%B6)
### More in our [wiki](https://github.com/2dust/v2rayNG/wiki)

10
V2rayNG/.gitignore vendored
View File

@@ -1,10 +0,0 @@
*.iml
.gradle
/local.properties
/.idea
.DS_Store
/build
/captures
*.apk
signing.properties
*.aar

View File

@@ -1,2 +0,0 @@
/build
/google-services.json

View File

@@ -12,8 +12,8 @@ android {
applicationId = "com.v2ray.ang"
minSdk = 21
targetSdk = 35
versionCode = 642
versionName = "1.9.42"
versionCode = 644
versionName = "1.9.44"
multiDexEnabled = true
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
@@ -82,8 +82,9 @@ android {
val isFdroid = variant.productFlavors.any { it.name == "fdroid" }
if (isFdroid) {
val versionCodes =
mapOf("armeabi-v7a" to 2, "arm64-v8a" to 1, "x86" to 4, "x86_64" to 3, "universal" to 0
)
mapOf(
"armeabi-v7a" to 2, "arm64-v8a" to 1, "x86" to 4, "x86_64" to 3, "universal" to 0
)
variant.outputs
.map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl }
@@ -148,7 +149,7 @@ dependencies {
// UI Libraries
implementation(libs.material)
implementation(libs.toastcompat)
implementation(libs.toasty)
implementation(libs.editorkit)
implementation(libs.flexbox)

View File

@@ -35,7 +35,6 @@
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- <useapplications-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission
@@ -52,10 +51,10 @@
android:banner="@mipmap/ic_banner"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:supportsRtl="true"
android:theme="@style/AppThemeDayNight"
android:usesCleartextTraffic="true"
android:networkSecurityConfig="@xml/network_security_config"
tools:targetApi="m">
<activity
@@ -213,7 +212,8 @@
android:icon="@drawable/ic_stat_name"
android:label="@string/app_tile_name"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
android:process=":RunSoLibV2RayDaemon">
android:process=":RunSoLibV2RayDaemon"
tools:targetApi="24">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>

View File

@@ -20,13 +20,6 @@
"port": "443",
"network": "udp"
},
{
"remarks": "阻断广告",
"outboundTag": "block",
"domain": [
"geosite:category-ads-all"
]
},
{
"remarks": "绕过局域网IP",
"outboundTag": "direct",

View File

@@ -5,13 +5,6 @@
"port": "443",
"network": "udp"
},
{
"remarks": "阻断广告",
"outboundTag": "block",
"domain": [
"geosite:category-ads-all"
]
},
{
"remarks": "绕过局域网IP",
"outboundTag": "direct",

View File

@@ -13,13 +13,6 @@
"port": "443",
"network": "udp"
},
{
"remarks": "阻断广告",
"outboundTag": "block",
"domain": [
"geosite:category-ads-all"
]
},
{
"remarks": "绕过局域网IP",
"outboundTag": "direct",

View File

@@ -5,13 +5,6 @@
"port": "443",
"network": "udp"
},
{
"remarks": "Block ads and trackers",
"outboundTag": "block",
"domain": [
"geosite:category-ads-all"
]
},
{
"remarks": "Direct LAN IP",
"outboundTag": "direct",

View File

@@ -5,6 +5,7 @@ object AppConfig {
/** The application's package name. */
const val ANG_PACKAGE = BuildConfig.APPLICATION_ID
const val TAG = BuildConfig.APPLICATION_ID
/** Directory names used in the app's file system. */
const val DIR_ASSETS = "assets"
@@ -169,7 +170,7 @@ object AppConfig {
const val DNS_QUAD9_DOMAIN = "dns.quad9.net"
const val DNS_YANDEX_DOMAIN = "common.dot.dns.yandex.net"
val DNS_ALIDNS_ADDRESSES = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1")
val DNS_CLOUDFLARE_ADDRESSES = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001")
val DNS_DNSPOD_ADDRESSES = arrayListOf("1.12.12.12", "120.53.53.53")

View File

@@ -11,7 +11,8 @@ enum class EConfigType(val value: Int, val protocolScheme: String) {
VLESS(5, AppConfig.VLESS),
TROJAN(6, AppConfig.TROJAN),
WIREGUARD(7, AppConfig.WIREGUARD),
// TUIC(8, AppConfig.TUIC),
// TUIC(8, AppConfig.TUIC),
HYSTERIA2(9, AppConfig.HYSTERIA2),
HTTP(10, AppConfig.HTTP);

View File

@@ -8,6 +8,7 @@ enum class Language(val code: String) {
VIETNAMESE("vi"),
RUSSIAN("ru"),
PERSIAN("fa"),
ARABIC("ar"),
BANGLA("bn"),
BAKHTIARI("bqi-rIR");

View File

@@ -8,6 +8,7 @@ enum class NetworkType(val type: String) {
XHTTP("xhttp"),
HTTP("http"),
H2("h2"),
//QUIC("quic"),
GRPC("grpc");

View File

@@ -22,7 +22,7 @@ data class V2rayConfig(
var policy: PolicyBean?,
val inbounds: ArrayList<InboundBean>,
var outbounds: ArrayList<OutboundBean>,
var dns: DnsBean,
var dns: DnsBean? = null,
val routing: RoutingBean,
val api: Any? = null,
val transport: Any? = null,
@@ -261,7 +261,8 @@ data class V2rayConfig(
) {
data class HeaderBean(
var type: String = "none",
var domain: String? = null)
var domain: String? = null
)
}
data class WsSettingsBean(

View File

@@ -8,7 +8,7 @@ import android.os.Build
import android.os.Bundle
import android.widget.Toast
import com.v2ray.ang.AngApplication
import me.drakeet.support.toast.ToastCompat
import es.dmoral.toasty.Toasty
import org.json.JSONObject
import java.io.Serializable
import java.net.URI
@@ -23,7 +23,7 @@ val Context.v2RayApplication: AngApplication?
* @param message The resource ID of the message to show.
*/
fun Context.toast(message: Int) {
ToastCompat.makeText(this, message, Toast.LENGTH_SHORT).show()
Toasty.normal(this, message).show()
}
/**
@@ -32,9 +32,46 @@ fun Context.toast(message: Int) {
* @param message The text of the message to show.
*/
fun Context.toast(message: CharSequence) {
ToastCompat.makeText(this, message, Toast.LENGTH_SHORT).show()
Toasty.normal(this, message).show()
}
/**
* Shows a toast message with the given resource ID.
*
* @param message The resource ID of the message to show.
*/
fun Context.toastSuccess(message: Int) {
Toasty.success(this, message, Toast.LENGTH_SHORT, true).show()
}
/**
* Shows a toast message with the given text.
*
* @param message The text of the message to show.
*/
fun Context.toastSuccess(message: CharSequence) {
Toasty.success(this, message, Toast.LENGTH_SHORT, true).show()
}
/**
* Shows a toast message with the given resource ID.
*
* @param message The resource ID of the message to show.
*/
fun Context.toastError(message: Int) {
Toasty.error(this, message, Toast.LENGTH_SHORT, true).show()
}
/**
* Shows a toast message with the given text.
*
* @param message The text of the message to show.
*/
fun Context.toastError(message: CharSequence) {
Toasty.error(this, message, Toast.LENGTH_SHORT, true).show()
}
/**
* Puts a key-value pair into the JSONObject.
*

View File

@@ -120,7 +120,7 @@ open class FmtBase {
config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
}
NetworkType.XHTTP -> {
NetworkType.XHTTP -> {
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
config.xhttpMode.let { if (it.isNotNullEmpty()) dicQuery["mode"] = it.orEmpty() }

View File

@@ -1,5 +1,7 @@
package com.v2ray.ang.fmt
import android.util.Log
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.NetworkType
import com.v2ray.ang.dto.ProfileItem
@@ -82,7 +84,7 @@ object ShadowsocksFmt : FmtBase() {
config.remarks =
Utils.urlDecode(result.substring(indexSplit + 1, result.length))
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to decode remarks in SS legacy URL", e)
}
result = result.substring(0, indexSplit)

View File

@@ -33,7 +33,7 @@ object VmessFmt : FmtBase() {
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")
Log.w(AppConfig.TAG, "Toast decoding failed")
return null
}
val vmessQRCode = JsonUtil.fromJson(result, VmessQRCode::class.java)
@@ -43,7 +43,7 @@ object VmessFmt : FmtBase() {
|| TextUtils.isEmpty(vmessQRCode.id)
|| TextUtils.isEmpty(vmessQRCode.net)
) {
Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_incorrect_protocol")
Log.w(AppConfig.TAG, "Toast incorrect protocol")
return null
}
@@ -73,6 +73,7 @@ object VmessFmt : FmtBase() {
config.serviceName = vmessQRCode.path
config.authority = vmessQRCode.host
}
else -> {}
}
@@ -119,6 +120,7 @@ object VmessFmt : FmtBase() {
vmessQRCode.path = config.serviceName.orEmpty()
vmessQRCode.host = config.authority.orEmpty()
}
else -> {}
}

View File

@@ -42,7 +42,7 @@ object AngConfigManager {
Utils.setClipboard(context, conf)
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to share config to clipboard", e)
return -1
}
return 0
@@ -71,7 +71,7 @@ object AngConfigManager {
}
return sb.lines().count()
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to share non-custom configs to clipboard", e)
return -1
}
}
@@ -91,7 +91,7 @@ object AngConfigManager {
return QRCodeDecoder.createQRCode(conf)
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to share config as QR code", e)
return null
}
}
@@ -120,7 +120,7 @@ object AngConfigManager {
return -1
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to share full content to clipboard", e)
return -1
}
return 0
@@ -148,7 +148,7 @@ object AngConfigManager {
EConfigType.HYSTERIA2 -> Hysteria2Fmt.toUri(config)
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to share config for GUID: $guid", e)
return ""
}
}
@@ -203,7 +203,7 @@ object AngConfigManager {
}
return count
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to parse batch subscription", e)
}
return 0
}
@@ -251,7 +251,7 @@ object AngConfigManager {
}
return count
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to parse batch config", e)
}
return 0
}
@@ -287,7 +287,7 @@ object AngConfigManager {
return count
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to parse custom config server JSON array", e)
}
try {
@@ -298,7 +298,7 @@ object AngConfigManager {
MmkvManager.encodeServerRaw(key, server)
return 1
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to parse custom config server as single config", e)
}
return 0
} else if (server.startsWith("[Interface]") && server.contains("[Peer]")) {
@@ -308,7 +308,7 @@ object AngConfigManager {
MmkvManager.encodeServerRaw(key, server)
return 1
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to parse WireGuard config file", e)
}
return 0
} else {
@@ -372,7 +372,7 @@ object AngConfigManager {
MmkvManager.setSelectServer(guid)
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to parse config", e)
return -1
}
return 0
@@ -390,7 +390,7 @@ object AngConfigManager {
count += updateConfigViaSub(it)
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to update config via all subscriptions", e)
return 0
}
return count
@@ -417,21 +417,20 @@ object AngConfigManager {
if (!Utils.isValidUrl(url)) {
return 0
}
Log.d(AppConfig.ANG_PACKAGE, url)
Log.i(AppConfig.TAG, url)
var configText = try {
val httpPort = SettingsManager.getHttpPort()
HttpUtil.getUrlContentWithUserAgent(url, 15000, httpPort)
} catch (e: Exception) {
Log.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error, try……")
//e.printStackTrace()
Log.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error", e)
""
}
if (configText.isEmpty()) {
configText = try {
HttpUtil.getUrlContentWithUserAgent(url)
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to get URL content with user agent", e)
""
}
}
@@ -440,7 +439,7 @@ object AngConfigManager {
}
return parseConfigViaSub(configText, it.first, false)
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to update config via subscription", e)
return 0
}
}

View File

@@ -3,7 +3,6 @@ package com.v2ray.ang.handler
import android.util.Log
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.NetworkType
import com.v2ray.ang.dto.ProfileItem
@@ -26,7 +25,7 @@ object MigrateManager {
return false
}
val serverList = serverStorage.allKeys() ?: return false
Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-" + serverList.count())
Log.i(AppConfig.TAG, "migrateServerConfig2Profile-" + serverList.count())
for (guid in serverList) {
var configOld = decodeServerConfigOld(guid) ?: continue
@@ -43,9 +42,9 @@ object MigrateManager {
//check and remove old
decodeServerConfig(guid) ?: continue
serverStorage.remove(guid)
Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-" + config.remarks)
Log.i(AppConfig.TAG, "migrateServerConfig2Profile-" + config.remarks)
}
Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-end")
Log.i(AppConfig.TAG, "migrateServerConfig2Profile-end")
return true
}
@@ -172,7 +171,7 @@ object MigrateManager {
outbound.settings?.let { wireguard ->
config.secretKey = wireguard.secretKey
config.localAddress = (wireguard.address as List<*>).joinToString(",").removeWhiteSpace().toString()
config.localAddress = (wireguard.address as List<*>).joinToString(",").removeWhiteSpace().toString()
config.publicKey = wireguard.peers?.getOrNull(0)?.publicKey
config.mtu = wireguard.mtu
config.reserved = wireguard.reserved?.joinToString(",").removeWhiteSpace().toString()

View File

@@ -571,7 +571,7 @@ object MmkvManager {
* @param startOnBoot Whether to start on boot.
*/
fun encodeStartOnBoot(startOnBoot: Boolean) {
MmkvManager.encodeSettings(PREF_IS_BOOTED, startOnBoot)
encodeSettings(PREF_IS_BOOTED, startOnBoot)
}
/**

View File

@@ -84,7 +84,7 @@ object SettingsManager {
resetRoutingRulesetsCommon(rulesetList)
return true
} catch (e: Exception) {
e.printStackTrace()
Log.e(ANG_PACKAGE, "Failed to reset routing rulesets", e)
return false
}
}
@@ -167,11 +167,11 @@ object SettingsManager {
}
val guid = MmkvManager.getSelectServer() ?: return false
val config = MmkvManager.decodeServerConfig(guid) ?: return false
val config = decodeServerConfig(guid) ?: return false
if (config.configType == EConfigType.CUSTOM) {
val raw = MmkvManager.decodeServerRaw(guid) ?: return false
val v2rayConfig = JsonUtil.fromJson(raw, V2rayConfig::class.java)
val exist = v2rayConfig.routing.rules.filter { it.outboundTag == TAG_DIRECT }?.any {
val exist = v2rayConfig.routing.rules.filter { it.outboundTag == TAG_DIRECT }.any {
it.domain?.contains(GEOSITE_PRIVATE) == true || it.ip?.contains(GEOIP_PRIVATE) == true
}
return exist == true
@@ -265,10 +265,7 @@ object SettingsManager {
input.copyTo(output)
}
}
Log.i(
ANG_PACKAGE,
"Copied from apk assets folder to ${target.absolutePath}"
)
Log.i(AppConfig.TAG, "Copied from apk assets folder to ${target.absolutePath}")
}
} catch (e: Exception) {
Log.e(ANG_PACKAGE, "asset copy failed", e)
@@ -343,6 +340,7 @@ object SettingsManager {
Language.VIETNAMESE -> Locale("vi")
Language.RUSSIAN -> Locale("ru")
Language.PERSIAN -> Locale("fa")
Language.ARABIC -> Locale("ar")
Language.BANGLA -> Locale("bn")
Language.BAKHTIARI -> Locale("bqi", "IR")
}

View File

@@ -51,7 +51,7 @@ object SpeedtestManager {
return try {
Libv2ray.measureOutboundDelay(config, SettingsManager.getDelayTestUrl())
} catch (e: Exception) {
Log.d(AppConfig.ANG_PACKAGE, "realPing: $e")
Log.e(AppConfig.TAG, "Failed to measure outbound delay", e)
-1L
}
}
@@ -76,7 +76,7 @@ object SpeedtestManager {
}
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to ping URL: $url", e)
}
return "-1ms"
}
@@ -103,11 +103,11 @@ object SpeedtestManager {
socket.close()
return time
} catch (e: UnknownHostException) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Unknown host: $url", e)
} catch (e: IOException) {
Log.d(AppConfig.ANG_PACKAGE, "socketConnectTime IOException: $e")
Log.e(AppConfig.TAG, "socketConnectTime IOException: $e")
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to establish socket connection to $url:$port", e)
}
return -1
}
@@ -152,13 +152,13 @@ object SpeedtestManager {
)
}
} catch (e: IOException) {
Log.d(AppConfig.ANG_PACKAGE, "testConnection IOException: " + Log.getStackTraceString(e))
Log.e(AppConfig.TAG, "Connection test IOException", e)
result = context.getString(R.string.connection_test_error, e.message)
} catch (e: Exception) {
Log.d(AppConfig.ANG_PACKAGE, "testConnection Exception: " + Log.getStackTraceString(e))
Log.e(AppConfig.TAG, "Connection test Exception", e)
result = context.getString(R.string.connection_test_error, e.message)
} finally {
conn?.disconnect()
conn.disconnect()
}
return Pair(elapsed, result)

View File

@@ -4,7 +4,6 @@ import android.content.Context
import android.text.TextUtils
import android.util.Log
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.AppConfig.DEFAULT_NETWORK
import com.v2ray.ang.AppConfig.DNS_ALIDNS_ADDRESSES
import com.v2ray.ang.AppConfig.DNS_ALIDNS_DOMAIN
@@ -52,6 +51,7 @@ import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.Utils
object V2rayConfigManager {
private var initConfigCache: String? = null
/**
* Retrieves the V2ray configuration for the given GUID.
@@ -63,47 +63,72 @@ object V2rayConfigManager {
fun getV2rayConfig(context: Context, guid: String): ConfigResult {
try {
val config = MmkvManager.decodeServerConfig(guid) ?: return ConfigResult(false)
if (config.configType == EConfigType.CUSTOM) {
val raw = MmkvManager.decodeServerRaw(guid) ?: return ConfigResult(false)
val domainPort = config.getServerAddressAndPort()
return ConfigResult(true, guid, raw, domainPort)
return if (config.configType == EConfigType.CUSTOM) {
getV2rayCustomConfig(guid, config)
} else {
getV2rayNormalConfig(context, guid, config)
}
val result = getV2rayNonCustomConfig(context, config)
//Log.d(ANG_PACKAGE, result.content)
result.guid = guid
return result
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to get V2ray config", e)
return ConfigResult(false)
}
}
/**
* Retrieves the non-custom V2ray configuration.
* Retrieves the speedtest V2ray configuration for the given GUID.
*
* @param context The context in which the function is called.
* @param context The context of the caller.
* @param guid The unique identifier for the V2ray configuration.
* @return A ConfigResult object containing the configuration details or indicating failure.
*/
fun getV2rayConfig4Speedtest(context: Context, guid: String): ConfigResult {
try {
val config = MmkvManager.decodeServerConfig(guid) ?: return ConfigResult(false)
return if (config.configType == EConfigType.CUSTOM) {
getV2rayCustomConfig(guid, config)
} else {
getV2rayNormalConfig4Speedtest(context, guid, config)
}
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to get V2ray config for speedtest", e)
return ConfigResult(false)
}
}
/**
* Retrieves the custom V2ray configuration.
*
* @param guid The unique identifier for the V2ray configuration.
* @param config The profile item containing the configuration details.
* @return A ConfigResult object containing the result of the configuration retrieval.
*/
private fun getV2rayNonCustomConfig(context: Context, config: ProfileItem): ConfigResult {
private fun getV2rayCustomConfig(guid: String, config: ProfileItem): ConfigResult {
val raw = MmkvManager.decodeServerRaw(guid) ?: return ConfigResult(false)
val domainPort = config.getServerAddressAndPort()
return ConfigResult(true, guid, raw, domainPort)
}
/**
* Retrieves the normal V2ray configuration.
*
* @param context The context in which the function is called.
* @param guid The unique identifier for the V2ray configuration.
* @param config The profile item containing the configuration details.
* @return A ConfigResult object containing the result of the configuration retrieval.
*/
private fun getV2rayNormalConfig(context: Context, guid: String, config: ProfileItem): ConfigResult {
val result = ConfigResult(false)
val address = config.server ?: return result
if (!Utils.isIpAddress(address)) {
if (!Utils.isValidUrl(address)) {
Log.d(ANG_PACKAGE, "$address is an invalid ip or domain")
Log.w(AppConfig.TAG, "$address is an invalid ip or domain")
return result
}
}
val assets = Utils.readTextFromAssets(context, "v2ray_config.json")
if (TextUtils.isEmpty(assets)) {
return result
}
val v2rayConfig = JsonUtil.fromJson(assets, V2rayConfig::class.java) ?: return result
v2rayConfig.log.loglevel =
MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL) ?: "warning"
val v2rayConfig = initV2rayConfig(context) ?: return result
v2rayConfig.log.loglevel = MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL) ?: "warning"
v2rayConfig.remarks = config.remarks
inbounds(v2rayConfig)
@@ -129,9 +154,75 @@ object V2rayConfigManager {
result.status = true
result.content = v2rayConfig.toPrettyPrinting()
result.domainPort = if (retMore.first) retMore.second else retOut.second
result.guid = guid
return result
}
/**
* Retrieves the normal V2ray configuration for speedtest.
*
* @param context The context in which the function is called.
* @param guid The unique identifier for the V2ray configuration.
* @param config The profile item containing the configuration details.
* @return A ConfigResult object containing the result of the configuration retrieval.
*/
private fun getV2rayNormalConfig4Speedtest(context: Context, guid: String, config: ProfileItem): ConfigResult {
val result = ConfigResult(false)
val address = config.server ?: return result
if (!Utils.isIpAddress(address)) {
if (!Utils.isValidUrl(address)) {
Log.w(AppConfig.TAG, "$address is an invalid ip or domain")
return result
}
}
val v2rayConfig = initV2rayConfig(context) ?: return result
val isPlugin = config.configType == EConfigType.HYSTERIA2
val retOut = outbounds(v2rayConfig, config, isPlugin) ?: return result
val retMore = moreOutbounds(v2rayConfig, config.subscriptionId, isPlugin)
v2rayConfig.log.loglevel = MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL) ?: "warning"
v2rayConfig.inbounds.clear()
v2rayConfig.routing.rules.clear()
v2rayConfig.dns = null
v2rayConfig.fakedns = null
v2rayConfig.stats = null
v2rayConfig.policy = null
v2rayConfig.outbounds.forEach { key ->
key.mux = null
}
result.status = true
result.content = v2rayConfig.toPrettyPrinting()
result.domainPort = if (retMore.first) retMore.second else retOut.second
result.guid = guid
return result
}
/**
* Initializes V2ray configuration.
*
* This function loads the V2ray configuration from assets or from a cached value.
* It first attempts to use the cached configuration if available, otherwise reads
* the configuration from the "v2ray_config.json" asset file.
*
* @param context Android context used to access application assets
* @return V2rayConfig object parsed from the JSON configuration, or null if the configuration is empty
*/
private fun initV2rayConfig(context: Context): V2rayConfig? {
val assets = initConfigCache ?: Utils.readTextFromAssets(context, "v2ray_config.json")
if (TextUtils.isEmpty(assets)) {
return null
}
initConfigCache = assets
val config = JsonUtil.fromJson(assets, V2rayConfig::class.java)
return config
}
private fun inbounds(v2rayConfig: V2rayConfig): Boolean {
try {
val socksPort = SettingsManager.getSocksPort()
@@ -164,7 +255,7 @@ object V2rayConfigManager {
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to configure inbounds", e)
return false
}
return true
@@ -227,7 +318,7 @@ object V2rayConfigManager {
routingUserRule(key, v2rayConfig)
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to configure routing", e)
return false
}
return true
@@ -244,7 +335,7 @@ object V2rayConfigManager {
v2rayConfig.routing.rules.add(rule)
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to apply routing user rule", e)
}
}
@@ -274,7 +365,7 @@ object V2rayConfigManager {
val proxyDomain = userRule2Domain(TAG_PROXY)
val directDomain = userRule2Domain(TAG_DIRECT)
// fakedns with all domains to make it always top priority
v2rayConfig.dns.servers?.add(
v2rayConfig.dns?.servers?.add(
0,
V2rayConfig.DnsBean.ServersBean(
address = "fakedns",
@@ -330,7 +421,7 @@ object V2rayConfigManager {
)
)
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to configure custom local DNS", e)
return false
}
return true
@@ -394,7 +485,7 @@ object V2rayConfigManager {
if (userHostsMap != null) hosts.putAll(userHostsMap)
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to configure user DNS hosts", e)
}
//block dns
@@ -433,7 +524,7 @@ object V2rayConfigManager {
)
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to configure DNS", e)
return false
}
return true
@@ -459,7 +550,7 @@ object V2rayConfigManager {
outbound.mux?.enabled = true
outbound.mux?.concurrency = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_CONCURRENCY, "8").orEmpty().toInt()
outbound.mux?.xudpConcurrency = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "16").orEmpty().toInt()
outbound.mux?.xudpProxyUDP443 = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_QUIC,"reject")
outbound.mux?.xudpProxyUDP443 = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_QUIC, "reject")
if (protocol.equals(EConfigType.VLESS.name, true) && outbound.settings?.vnext?.first()?.users?.first()?.flow?.isNotEmpty() == true) {
outbound.mux?.concurrency = -1
}
@@ -504,7 +595,7 @@ object V2rayConfigManager {
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to update outbound with global settings", e)
return false
}
return true
@@ -570,7 +661,7 @@ object V2rayConfigManager {
dialerProxy = TAG_FRAGMENT
)
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to update outbound fragment", e)
return false
}
return true
@@ -633,7 +724,7 @@ object V2rayConfigManager {
}
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to configure more outbounds", e)
return returnPair
}

View File

@@ -35,7 +35,7 @@ class TaskerReceiver : BroadcastReceiver() {
V2RayServiceManager.stopVService(context)
}
} catch (e: Exception) {
e.printStackTrace()
android.util.Log.e(AppConfig.TAG, "Error processing Tasker broadcast", e)
}
}
}

View File

@@ -2,7 +2,7 @@ package com.v2ray.ang.service
import android.content.Context
import android.util.Log
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.AppConfig
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -16,7 +16,7 @@ class ProcessService {
* @param cmd The command to run.
*/
fun runProcess(context: Context, cmd: MutableList<String>) {
Log.d(ANG_PACKAGE, cmd.toString())
Log.i(AppConfig.TAG, cmd.toString())
try {
val proBuilder = ProcessBuilder(cmd)
@@ -27,14 +27,14 @@ class ProcessService {
CoroutineScope(Dispatchers.IO).launch {
Thread.sleep(50L)
Log.d(ANG_PACKAGE, "runProcess check")
Log.i(AppConfig.TAG, "runProcess check")
process?.waitFor()
Log.d(ANG_PACKAGE, "runProcess exited")
Log.i(AppConfig.TAG, "runProcess exited")
}
Log.d(ANG_PACKAGE, process.toString())
Log.i(AppConfig.TAG, process.toString())
} catch (e: Exception) {
Log.d(ANG_PACKAGE, e.toString())
Log.e(AppConfig.TAG, e.toString(), e)
}
}
@@ -43,10 +43,10 @@ class ProcessService {
*/
fun stopProcess() {
try {
Log.d(ANG_PACKAGE, "runProcess destroy")
Log.i(AppConfig.TAG, "runProcess destroy")
process?.destroy()
} catch (e: Exception) {
Log.d(ANG_PACKAGE, e.toString())
Log.e(AppConfig.TAG, "Failed to destroy process", e)
}
}
}

View File

@@ -8,6 +8,7 @@ import android.graphics.drawable.Icon
import android.os.Build
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import com.v2ray.ang.AppConfig
@@ -61,7 +62,7 @@ class QSTileService : TileService() {
applicationContext.unregisterReceiver(mMsgReceive)
mMsgReceive = null
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to unregister receiver", e)
}
}

View File

@@ -38,7 +38,7 @@ object SubscriptionUpdater {
*/
@SuppressLint("MissingPermission")
override suspend fun doWork(): Result {
Log.d(AppConfig.ANG_PACKAGE, "subscription automatic update starting")
Log.i(AppConfig.TAG, "subscription automatic update starting")
val subs = MmkvManager.decodeSubscriptions().filter { it.second.autoUpdate }
@@ -56,10 +56,7 @@ object SubscriptionUpdater {
notificationManager.createNotificationChannel(channel)
}
notificationManager.notify(3, notification.build())
Log.d(
AppConfig.ANG_PACKAGE,
"subscription automatic update: ---${subItem.remarks}"
)
Log.i(AppConfig.TAG, "subscription automatic update: ---${subItem.remarks}")
updateConfigViaSub(Pair(sub.first, subItem))
notification.setContentText("Updating ${subItem.remarks}")
}

View File

@@ -9,7 +9,6 @@ import android.os.Build
import android.util.Log
import androidx.core.content.ContextCompat
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.R
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ProfileItem
@@ -144,7 +143,7 @@ object V2RayServiceManager {
mFilter.addAction(Intent.ACTION_USER_PRESENT)
ContextCompat.registerReceiver(service, mMsgReceive, mFilter, Utils.receiverFlags())
} catch (e: Exception) {
Log.d(ANG_PACKAGE, e.toString())
Log.e(AppConfig.TAG, "Failed to register broadcast receiver", e)
}
v2rayPoint.configureFileContent = result.content
@@ -154,7 +153,7 @@ object V2RayServiceManager {
try {
v2rayPoint.runLoop(MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6))
} catch (e: Exception) {
Log.d(ANG_PACKAGE, e.toString())
Log.e(AppConfig.TAG, "Failed to start V2Ray loop", e)
}
if (v2rayPoint.isRunning) {
@@ -179,7 +178,7 @@ object V2RayServiceManager {
try {
v2rayPoint.stopLoop()
} catch (e: Exception) {
Log.d(ANG_PACKAGE, e.toString())
Log.e(AppConfig.TAG, "Failed to stop V2Ray loop", e)
}
}
}
@@ -190,7 +189,7 @@ object V2RayServiceManager {
try {
service.unregisterReceiver(mMsgReceive)
} catch (e: Exception) {
Log.d(ANG_PACKAGE, e.toString())
Log.e(AppConfig.TAG, "Failed to unregister broadcast receiver", e)
}
PluginUtil.stopPlugin()
}
@@ -217,14 +216,14 @@ object V2RayServiceManager {
try {
time = v2rayPoint.measureDelay(SettingsManager.getDelayTestUrl())
} catch (e: Exception) {
Log.d(ANG_PACKAGE, "measureV2rayDelay: $e")
Log.e(AppConfig.TAG, "Failed to measure delay with primary URL", e)
errstr = e.message?.substringAfter("\":") ?: "empty message"
}
if (time == -1L) {
try {
time = v2rayPoint.measureDelay(SettingsManager.getDelayTestUrl(true))
} catch (e: Exception) {
Log.d(ANG_PACKAGE, "measureV2rayDelay: $e")
Log.e(AppConfig.TAG, "Failed to measure delay with alternative URL", e)
errstr = e.message?.substringAfter("\":") ?: "empty message"
}
}
@@ -255,7 +254,7 @@ object V2RayServiceManager {
serviceControl.stopService()
0
} catch (e: Exception) {
Log.d(ANG_PACKAGE, e.toString())
Log.e(AppConfig.TAG, "Failed to stop service in callback", e)
-1
}
}
@@ -291,7 +290,7 @@ object V2RayServiceManager {
NotificationService.startSpeedNotification(currentConfig)
0
} catch (e: Exception) {
Log.d(ANG_PACKAGE, e.toString())
Log.e(AppConfig.TAG, "Failed to setup service in callback", e)
-1
}
}
@@ -323,12 +322,12 @@ object V2RayServiceManager {
}
AppConfig.MSG_STATE_STOP -> {
Log.d(ANG_PACKAGE, "Stop Service")
Log.i(AppConfig.TAG, "Stop Service")
serviceControl.stopService()
}
AppConfig.MSG_STATE_RESTART -> {
Log.d(ANG_PACKAGE, "Restart Service")
Log.i(AppConfig.TAG, "Restart Service")
serviceControl.stopService()
Thread.sleep(500L)
startVService(serviceControl.getService())
@@ -341,12 +340,12 @@ object V2RayServiceManager {
when (intent?.action) {
Intent.ACTION_SCREEN_OFF -> {
Log.d(ANG_PACKAGE, "SCREEN_OFF, stop querying stats")
Log.i(AppConfig.TAG, "SCREEN_OFF, stop querying stats")
NotificationService.stopSpeedNotification(currentConfig)
}
Intent.ACTION_SCREEN_ON -> {
Log.d(ANG_PACKAGE, "SCREEN_ON, start querying stats")
Log.i(AppConfig.TAG, "SCREEN_ON, start querying stats")
NotificationService.startSpeedNotification(currentConfig)
}
}

View File

@@ -81,11 +81,11 @@ class V2RayTestService : Service() {
val delay = PluginUtil.realPingHy2(this, config)
return delay
} else {
val config = V2rayConfigManager.getV2rayConfig(this, guid)
if (!config.status) {
val configResult = V2rayConfigManager.getV2rayConfig4Speedtest(this, guid)
if (!configResult.status) {
return retFailure
}
return SpeedtestManager.realPing(config.content)
return SpeedtestManager.realPing(configResult.content)
}
}
}

View File

@@ -18,7 +18,6 @@ import android.os.StrictMode
import android.util.Log
import androidx.annotation.RequiresApi
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.AppConfig.LOOPBACK
import com.v2ray.ang.BuildConfig
import com.v2ray.ang.R
@@ -40,6 +39,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
private const val PRIVATE_VLAN6_CLIENT = "fc00::10:10:14:1"
private const val PRIVATE_VLAN6_ROUTER = "fc00::10:10:14:2"
private const val TUN2SOCKS = "libtun2socks.so"
}
private lateinit var mInterface: ParcelFileDescriptor
@@ -209,7 +209,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
else
builder.addAllowedApplication(it)
} catch (e: PackageManager.NameNotFoundException) {
Log.d(ANG_PACKAGE, "setup error : --${e.localizedMessage}")
Log.e(AppConfig.TAG, "Failed to configure app in VPN: ${e.localizedMessage}", e)
}
}
} else {
@@ -227,7 +227,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
try {
connectivity.requestNetwork(defaultNetworkRequest, defaultNetworkCallback)
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to request default network", e)
}
}
@@ -245,7 +245,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
return true
} catch (e: Exception) {
// non-nullable lateinit var
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to establish VPN interface", e)
stopV2Ray()
}
return false
@@ -277,7 +277,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
cmd.add("--dnsgw")
cmd.add("$LOOPBACK:${localDnsPort}")
}
Log.d(packageName, cmd.toString())
Log.i(AppConfig.TAG, cmd.toString())
try {
val proBuilder = ProcessBuilder(cmd)
@@ -286,19 +286,19 @@ class V2RayVpnService : VpnService(), ServiceControl {
.directory(applicationContext.filesDir)
.start()
Thread {
Log.d(packageName, "$TUN2SOCKS check")
Log.i(AppConfig.TAG, "$TUN2SOCKS check")
process.waitFor()
Log.d(packageName, "$TUN2SOCKS exited")
Log.i(AppConfig.TAG, "$TUN2SOCKS exited")
if (isRunning) {
Log.d(packageName, "$TUN2SOCKS restart")
Log.i(AppConfig.TAG, "$TUN2SOCKS restart")
runTun2socks()
}
}.start()
Log.d(packageName, process.toString())
Log.i(AppConfig.TAG, process.toString())
sendFd()
} catch (e: Exception) {
Log.d(packageName, e.toString())
Log.e(AppConfig.TAG, "Failed to start tun2socks process", e)
}
}
@@ -309,13 +309,13 @@ class V2RayVpnService : VpnService(), ServiceControl {
private fun sendFd() {
val fd = mInterface.fileDescriptor
val path = File(applicationContext.filesDir, "sock_path").absolutePath
Log.d(packageName, path)
Log.i(AppConfig.TAG, path)
CoroutineScope(Dispatchers.IO).launch {
var tries = 0
while (true) try {
Thread.sleep(50L shl tries)
Log.d(packageName, "sendFd tries: $tries")
Log.i(AppConfig.TAG, "sendFd tries: $tries")
LocalSocket().use { localSocket ->
localSocket.connect(LocalSocketAddress(path, LocalSocketAddress.Namespace.FILESYSTEM))
localSocket.setFileDescriptorsForSend(arrayOf(fd))
@@ -323,7 +323,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
}
break
} catch (e: Exception) {
Log.d(packageName, e.toString())
Log.e(AppConfig.TAG, "Failed to send file descriptor, try: $tries", e)
if (tries > 5) break
tries += 1
}
@@ -349,10 +349,10 @@ class V2RayVpnService : VpnService(), ServiceControl {
}
try {
Log.d(packageName, "tun2socks destroy")
Log.i(AppConfig.TAG, "tun2socks destroy")
process.destroy()
} catch (e: Exception) {
Log.d(packageName, e.toString())
Log.e(AppConfig.TAG, "Failed to destroy tun2socks process", e)
}
V2RayServiceManager.stopV2rayPoint()
@@ -367,8 +367,8 @@ class V2RayVpnService : VpnService(), ServiceControl {
try {
mInterface.close()
} catch (ignored: Exception) {
// ignored
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to close VPN interface", e)
}
}
}

View File

@@ -14,6 +14,8 @@ import com.v2ray.ang.BuildConfig
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityAboutBinding
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastError
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.SpeedtestManager
import com.v2ray.ang.util.Utils
import com.v2ray.ang.util.ZipUtil
@@ -32,7 +34,7 @@ class AboutActivity : BaseActivity() {
try {
showFileChooser()
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to show file chooser", e)
}
} else {
toast(R.string.toast_permission_denied)
@@ -50,9 +52,9 @@ class AboutActivity : BaseActivity() {
binding.layoutBackup.setOnClickListener {
val ret = backupConfiguration(extDir.absolutePath)
if (ret.first) {
toast(R.string.toast_success)
toastSuccess(R.string.toast_success)
} else {
toast(R.string.toast_failure)
toastError(R.string.toast_failure)
}
}
@@ -72,7 +74,7 @@ class AboutActivity : BaseActivity() {
)
)
} else {
toast(R.string.toast_failure)
toastError(R.string.toast_failure)
}
}
@@ -88,7 +90,7 @@ class AboutActivity : BaseActivity() {
try {
showFileChooser()
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to show file chooser", e)
}
} else {
requestPermissionLauncher.launch(permission)
@@ -104,7 +106,7 @@ class AboutActivity : BaseActivity() {
}
binding.layoutOssLicenses.setOnClickListener {
val webView = android.webkit.WebView(this);
val webView = android.webkit.WebView(this)
webView.loadUrl("file:///android_asset/open_source_licenses.html")
android.app.AlertDialog.Builder(this)
.setTitle("Open source licenses")
@@ -126,7 +128,7 @@ class AboutActivity : BaseActivity() {
}
}
fun backupConfiguration(outputZipFilePos: String): Pair<Boolean, String> {
private fun backupConfiguration(outputZipFilePos: String): Pair<Boolean, String> {
val dateFormated = SimpleDateFormat(
"yyyy-MM-dd-HH-mm-ss",
Locale.getDefault()
@@ -147,7 +149,7 @@ class AboutActivity : BaseActivity() {
}
}
fun restoreConfiguration(zipFile: File): Boolean {
private fun restoreConfiguration(zipFile: File): Boolean {
val backupDir = this.cacheDir.absolutePath + "/${System.currentTimeMillis()}"
if (!ZipUtil.unzipToFolder(zipFile, backupDir)) {
@@ -167,7 +169,7 @@ class AboutActivity : BaseActivity() {
try {
chooseFile.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser)))
} catch (ex: android.content.ActivityNotFoundException) {
Log.e(AppConfig.ANG_PACKAGE, "File chooser activity not found: ${ex.message}", ex)
Log.e(AppConfig.TAG, "File chooser activity not found", ex)
toast(R.string.toast_require_file_manager)
}
}
@@ -185,13 +187,13 @@ class AboutActivity : BaseActivity() {
}
}
if (restoreConfiguration(targetFile)) {
toast(R.string.toast_success)
toastSuccess(R.string.toast_success)
} else {
toast(R.string.toast_failure)
toastError(R.string.toast_failure)
}
} catch (e: Exception) {
Log.e(AppConfig.ANG_PACKAGE, "Error during file restore: ${e.message}", e)
toast(R.string.toast_failure)
Log.e(AppConfig.TAG, "Error during file restore", e)
toastError(R.string.toast_failure)
}
}
}

View File

@@ -1,16 +1,19 @@
package com.v2ray.ang.ui
import android.annotation.SuppressLint
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.widget.SearchView
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityLogcatBinding
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.util.Utils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -21,7 +24,7 @@ import java.io.IOException
class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
private val binding by lazy { ActivityLogcatBinding.inflate(layoutInflater) }
var logsetsAll: MutableList<String> = mutableListOf()
private var logsetsAll: MutableList<String> = mutableListOf()
var logsets: MutableList<String> = mutableListOf()
private val adapter by lazy { LogcatRecyclerAdapter(this) }
@@ -62,12 +65,12 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
launch(Dispatchers.Main) {
logsetsAll = allText.toMutableList()
logsets = allText.toMutableList()
adapter.notifyDataSetChanged()
refreshData()
binding.refreshLayout.isRefreshing = false
}
}
} catch (e: IOException) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to get logcat", e)
}
}
@@ -84,11 +87,11 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
launch(Dispatchers.Main) {
logsetsAll.clear()
logsets.clear()
adapter.notifyDataSetChanged()
refreshData()
}
}
} catch (e: IOException) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to clear logcat", e)
}
}
@@ -118,7 +121,7 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.copy_all -> {
Utils.setClipboard(this, logsets.joinToString("\n"))
toast(R.string.toast_success)
toastSuccess(R.string.toast_success)
true
}
@@ -138,11 +141,16 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
logsetsAll.filter { it.contains(key) }.toMutableList()
}
adapter?.notifyDataSetChanged()
refreshData()
return true
}
override fun onRefresh() {
getLogcat()
}
@SuppressLint("NotifyDataSetChanged")
fun refreshData() {
adapter.notifyDataSetChanged()
}
}

View File

@@ -1,13 +1,16 @@
package com.v2ray.ang.ui
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.v2ray.ang.AppConfig
import com.v2ray.ang.databinding.ItemRecyclerLogcatBinding
class LogcatRecyclerAdapter(val activity: LogcatActivity) : RecyclerView.Adapter<LogcatRecyclerAdapter.MainViewHolder>() {
private var mActivity: LogcatActivity = activity
override fun getItemCount() = mActivity.logsets.size
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
@@ -22,7 +25,7 @@ class LogcatRecyclerAdapter(val activity: LogcatActivity) : RecyclerView.Adapter
holder.itemSubSettingBinding.logContent.text = if (content.count() > 1) content.last().trim() else ""
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Error binding log view data", e)
}
}

View File

@@ -1,6 +1,7 @@
package com.v2ray.ang.ui
import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.ColorStateList
@@ -8,6 +9,7 @@ import android.net.Uri
import android.net.VpnService
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.KeyEvent
import android.view.Menu
import android.view.MenuItem
@@ -31,6 +33,7 @@ import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityMainBinding
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastError
import com.v2ray.ang.handler.AngConfigManager
import com.v2ray.ang.handler.MigrateManager
import com.v2ray.ang.handler.MmkvManager
@@ -204,6 +207,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
})
}
@SuppressLint("NotifyDataSetChanged")
private fun setupViewModel() {
mainViewModel.updateListAction.observe(this) { index ->
if (index >= 0) {
@@ -269,7 +273,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
binding.tabGroup.isVisible = true
}
fun startV2Ray() {
private fun startV2Ray() {
if (MmkvManager.getSelectServer().isNullOrEmpty()) {
toast(R.string.title_file_chooser)
return
@@ -277,7 +281,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
V2RayServiceManager.startVService(this)
}
fun restartV2Ray() {
private fun restartV2Ray() {
if (mainViewModel.isRunning.value == true) {
V2RayServiceManager.stopVService(this)
}
@@ -482,7 +486,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
val clipboard = Utils.getClipboard(this)
importBatchConfig(clipboard)
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to import config from clipboard", e)
return false
}
return true
@@ -503,25 +507,28 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
}
countSub > 0 -> initGroupTab()
else -> toast(R.string.toast_failure)
else -> toastError(R.string.toast_failure)
}
binding.pbWaiting.hide()
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
toast(R.string.toast_failure)
toastError(R.string.toast_failure)
binding.pbWaiting.hide()
}
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to import batch config", e)
}
}
}
/**
* import config from local config file
*/
private fun importConfigLocal(): Boolean {
try {
showFileChooser()
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to import config from local file", e)
return false
}
return true
@@ -613,7 +620,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
toast(getString(R.string.title_update_config_count, count))
mainViewModel.reloadServerList()
} else {
toast(R.string.toast_failure)
toastError(R.string.toast_failure)
}
binding.pbWaiting.hide()
}
@@ -629,7 +636,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
if (ret > 0)
toast(getString(R.string.title_export_config_count, ret))
else
toast(R.string.toast_failure)
toastError(R.string.toast_failure)
binding.pbWaiting.hide()
}
}
@@ -741,7 +748,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
importBatchConfig(input?.bufferedReader()?.readText())
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to read content from URI", e)
}
} else {
requestPermissionLauncher.launch(permission)
@@ -759,9 +766,9 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
// }
// if (mainViewModel.appendCustomConfigServer(server)) {
// mainViewModel.reloadServerList()
// toast(R.string.toast_success)
// toastSuccess(R.string.toast_success)
// } else {
// toast(R.string.toast_failure)
// toastError(R.string.toast_failure)
// }
// //adapter.notifyItemInserted(mainViewModel.serverList.lastIndex)
// } catch (e: Exception) {
@@ -797,13 +804,14 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
// Handle navigation view item clicks here.
when (item.itemId) {
R.id.sub_setting -> requestSubSettingActivity.launch(Intent(this, SubSettingActivity::class.java))
R.id.per_app_proxy_settings -> startActivity(Intent(this, PerAppProxyActivity::class.java))
R.id.routing_setting -> requestSubSettingActivity.launch(Intent(this, RoutingSettingActivity::class.java))
R.id.user_asset_setting -> startActivity(Intent(this, UserAssetActivity::class.java))
R.id.settings -> startActivity(
Intent(this, SettingsActivity::class.java)
.putExtra("isRunning", mainViewModel.isRunning.value == true)
)
R.id.per_app_proxy_settings -> startActivity(Intent(this, PerAppProxyActivity::class.java))
R.id.routing_setting -> requestSubSettingActivity.launch(Intent(this, RoutingSettingActivity::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))

View File

@@ -3,6 +3,7 @@ package com.v2ray.ang.ui
import android.content.Intent
import android.graphics.Color
import android.text.TextUtils
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -19,6 +20,8 @@ import com.v2ray.ang.databinding.ItemRecyclerMainBinding
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastError
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.AngConfigManager
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.helper.ItemTouchHelperAdapter
@@ -43,6 +46,10 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
var isRunning = false
private val doubleColumnDisplay = MmkvManager.decodeSettingsBool(AppConfig.PREF_DOUBLE_COLUMN_DISPLAY, false)
/**
* Gets the total number of items in the adapter (servers count + footer view)
* @return The total item count
*/
override fun getItemCount() = mActivity.mainViewModel.serversCache.size + 1
override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
@@ -128,6 +135,12 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
// }
}
/**
* Gets the server address information
* Hides part of IP or domain information for privacy protection
* @param profile The server configuration
* @return Formatted address string
*/
private fun getAddress(profile: ProfileItem): String {
// Hide xxx:xxx:***/xxx.xxx.xxx.***
return "${
@@ -140,6 +153,11 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
} : ${profile.serverPort}"
}
/**
* Gets the subscription remarks information
* @param profile The server configuration
* @return Subscription remarks string, or empty string if none
*/
private fun getSubscriptionRemarks(profile: ProfileItem): String {
val subRemarks =
if (mActivity.mainViewModel.subscriptionId.isEmpty())
@@ -149,6 +167,15 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
return subRemarks?.toString() ?: ""
}
/**
* Shares server configuration
* Displays a dialog with sharing options and executes the selected action
* @param guid The server unique identifier
* @param profile The server configuration
* @param position The position in the list
* @param shareOptions The list of share options
* @param skip The number of options to skip
*/
private fun shareServer(guid: String, profile: ProfileItem, position: Int, shareOptions: List<String>, skip: Int) {
AlertDialog.Builder(mActivity).setItems(shareOptions.toTypedArray()) { _, i ->
try {
@@ -161,33 +188,51 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
else -> mActivity.toast("else")
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Error when sharing server", e)
}
}.show()
}
/**
* Displays QR code for the server configuration
* @param guid The server unique identifier
*/
private fun showQRCode(guid: String) {
val ivBinding = ItemQrcodeBinding.inflate(LayoutInflater.from(mActivity))
ivBinding.ivQcode.setImageBitmap(AngConfigManager.share2QRCode(guid))
AlertDialog.Builder(mActivity).setView(ivBinding.root).show()
}
/**
* Shares server configuration to clipboard
* @param guid The server unique identifier
*/
private fun share2Clipboard(guid: String) {
if (AngConfigManager.share2Clipboard(mActivity, guid) == 0) {
mActivity.toast(R.string.toast_success)
mActivity.toastSuccess(R.string.toast_success)
} else {
mActivity.toast(R.string.toast_failure)
mActivity.toastError(R.string.toast_failure)
}
}
/**
* Shares full server configuration content to clipboard
* @param guid The server unique identifier
*/
private fun shareFullContent(guid: String) {
if (AngConfigManager.shareFullContent2Clipboard(mActivity, guid) == 0) {
mActivity.toast(R.string.toast_success)
mActivity.toastSuccess(R.string.toast_success)
} else {
mActivity.toast(R.string.toast_failure)
mActivity.toastError(R.string.toast_failure)
}
}
/**
* Edits server configuration
* Opens appropriate editing interface based on configuration type
* @param guid The server unique identifier
* @param profile The server configuration
*/
private fun editServer(guid: String, profile: ProfileItem) {
val intent = Intent().putExtra("guid", guid)
.putExtra("isRunning", isRunning)
@@ -199,6 +244,12 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
}
}
/**
* Removes server configuration
* Handles confirmation dialog and related checks
* @param guid The server unique identifier
* @param position The position in the list
*/
private fun removeServer(guid: String, position: Int) {
if (guid != MmkvManager.getSelectServer()) {
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
@@ -218,12 +269,22 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
}
}
/**
* Executes the actual server removal process
* @param guid The server unique identifier
* @param position The position in the list
*/
private fun removeServerSub(guid: String, position: Int) {
mActivity.mainViewModel.removeServer(guid)
notifyItemRemoved(position)
notifyItemRangeChanged(position, mActivity.mainViewModel.serversCache.size)
}
/**
* Sets the selected server
* Updates UI and restarts service if needed
* @param guid The server unique identifier to select
*/
private fun setSelectServer(guid: String) {
val selected = MmkvManager.getSelectServer()
if (guid != selected) {
@@ -239,7 +300,7 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
delay(500)
V2RayServiceManager.startVService(mActivity)
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to restart V2Ray service", e)
}
}
}

View File

@@ -1,5 +1,6 @@
package com.v2ray.ang.ui
import android.annotation.SuppressLint
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
@@ -13,6 +14,7 @@ import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityBypassListBinding
import com.v2ray.ang.dto.AppInfo
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.extension.v2RayApplication
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsManager
@@ -29,6 +31,7 @@ class PerAppProxyActivity : BaseActivity() {
ActivityBypassListBinding.inflate(layoutInflater)
}
private var adapter: PerAppProxyAdapter? = null
private var appsAll: List<AppInfo>? = null
@@ -51,13 +54,13 @@ class PerAppProxyActivity : BaseActivity() {
appsList.forEach { app ->
app.isSelected = if (blacklist.contains(app.packageName)) 1 else 0
}
appsList.sortedWith(Comparator { p1, p2 ->
appsList.sortedWith { p1, p2 ->
when {
p1.isSelected > p2.isSelected -> -1
p1.isSelected == p2.isSelected -> 0
else -> 1
}
})
}
} else {
val collator = Collator.getInstance()
appsList.sortedWith(compareBy(collator) { it.appName })
@@ -112,8 +115,10 @@ class PerAppProxyActivity : BaseActivity() {
return super.onCreateOptionsMenu(menu)
}
@SuppressLint("NotifyDataSetChanged")
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.select_all -> adapter?.let {
R.id.select_all -> adapter?.let { it ->
val pkgNames = it.apps.map { it.packageName }
if (it.blacklist.containsAll(pkgNames)) {
it.apps.forEach {
@@ -160,9 +165,9 @@ class PerAppProxyActivity : BaseActivity() {
content = HttpUtil.getUrlContent(url, 5000, httpPort) ?: ""
}
launch(Dispatchers.Main) {
Log.d(ANG_PACKAGE, content)
Log.i(AppConfig.TAG, content)
selectProxyApp(content, true)
toast(R.string.toast_success)
toastSuccess(R.string.toast_success)
binding.pbWaiting.hide()
}
}
@@ -172,7 +177,7 @@ class PerAppProxyActivity : BaseActivity() {
val content = Utils.getClipboard(applicationContext)
if (TextUtils.isEmpty(content)) return
selectProxyApp(content, false)
toast(R.string.toast_success)
toastSuccess(R.string.toast_success)
}
private fun exportProxyApp() {
@@ -182,9 +187,10 @@ class PerAppProxyActivity : BaseActivity() {
lst = lst + System.getProperty("line.separator") + it
}
Utils.setClipboard(applicationContext, lst)
toast(R.string.toast_success)
toastSuccess(R.string.toast_success)
}
@SuppressLint("NotifyDataSetChanged")
private fun selectProxyApp(content: String, force: Boolean): Boolean {
try {
val proxyApps = if (TextUtils.isEmpty(content)) {
@@ -197,10 +203,10 @@ class PerAppProxyActivity : BaseActivity() {
adapter?.blacklist?.clear()
if (binding.switchBypassApps.isChecked) {
adapter?.let {
adapter?.let { it ->
it.apps.forEach block@{
val packageName = it.packageName
Log.d(ANG_PACKAGE, packageName)
Log.i(AppConfig.TAG, packageName)
if (!inProxyApps(proxyApps, packageName, force)) {
adapter?.blacklist?.add(packageName)
println(packageName)
@@ -210,10 +216,10 @@ class PerAppProxyActivity : BaseActivity() {
it.notifyDataSetChanged()
}
} else {
adapter?.let {
adapter?.let { it ->
it.apps.forEach block@{
val packageName = it.packageName
Log.d(ANG_PACKAGE, packageName)
Log.i(AppConfig.TAG, packageName)
if (inProxyApps(proxyApps, packageName, force)) {
adapter?.blacklist?.add(packageName)
println(packageName)
@@ -224,7 +230,7 @@ class PerAppProxyActivity : BaseActivity() {
}
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Error selecting proxy app", e)
return false
}
return true
@@ -259,7 +265,12 @@ class PerAppProxyActivity : BaseActivity() {
adapter = PerAppProxyAdapter(this, apps, adapter?.blacklist)
binding.recyclerView.adapter = adapter
adapter?.notifyDataSetChanged()
refreshData()
return true
}
@SuppressLint("NotifyDataSetChanged")
fun refreshData() {
adapter?.notifyDataSetChanged()
}
}

View File

@@ -9,6 +9,7 @@ import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityRoutingEditBinding
import com.v2ray.ang.dto.RulesetItem
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.util.Utils
import kotlinx.coroutines.Dispatchers
@@ -78,7 +79,7 @@ class RoutingEditActivity : BaseActivity() {
}
SettingsManager.saveRoutingRuleset(position, rulesetItem)
toast(R.string.toast_success)
toastSuccess(R.string.toast_success)
finish()
return true
}

View File

@@ -1,8 +1,10 @@
package com.v2ray.ang.ui
import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
@@ -17,6 +19,8 @@ import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityRoutingSettingBinding
import com.v2ray.ang.dto.RulesetItem
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastError
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
@@ -87,7 +91,6 @@ class RoutingSettingActivity : BaseActivity() {
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
R.id.add_rule -> startActivity(Intent(this, RoutingEditActivity::class.java)).let { true }
R.id.user_asset_setting -> startActivity(Intent(this, UserAssetActivity::class.java)).let { true }
R.id.import_predefined_rulesets -> importPredefined().let { true }
R.id.import_rulesets_from_clipboard -> importFromClipboard().let { true }
R.id.import_rulesets_from_qrcode -> requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA).let { true }
@@ -104,11 +107,11 @@ class RoutingSettingActivity : BaseActivity() {
SettingsManager.resetRoutingRulesetsFromPresets(this@RoutingSettingActivity, i)
launch(Dispatchers.Main) {
refreshData()
toast(R.string.toast_success)
toastSuccess(R.string.toast_success)
}
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to import predefined ruleset", e)
}
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
@@ -124,8 +127,8 @@ class RoutingSettingActivity : BaseActivity() {
val clipboard = try {
Utils.getClipboard(this)
} catch (e: Exception) {
e.printStackTrace()
toast(R.string.toast_failure)
Log.e(AppConfig.TAG, "Failed to get clipboard content", e)
toastError(R.string.toast_failure)
return@setPositiveButton
}
lifecycleScope.launch(Dispatchers.IO) {
@@ -133,9 +136,9 @@ class RoutingSettingActivity : BaseActivity() {
withContext(Dispatchers.Main) {
if (result) {
refreshData()
toast(R.string.toast_success)
toastSuccess(R.string.toast_success)
} else {
toast(R.string.toast_failure)
toastError(R.string.toast_failure)
}
}
}
@@ -149,10 +152,10 @@ class RoutingSettingActivity : BaseActivity() {
private fun export2Clipboard() {
val rulesetList = MmkvManager.decodeRoutingRulesets()
if (rulesetList.isNullOrEmpty()) {
toast(R.string.toast_failure)
toastError(R.string.toast_failure)
} else {
Utils.setClipboard(this, JsonUtil.toJson(rulesetList))
toast(R.string.toast_success)
toastSuccess(R.string.toast_success)
}
}
@@ -170,9 +173,9 @@ class RoutingSettingActivity : BaseActivity() {
withContext(Dispatchers.Main) {
if (result) {
refreshData()
toast(R.string.toast_success)
toastSuccess(R.string.toast_success)
} else {
toast(R.string.toast_failure)
toastError(R.string.toast_failure)
}
}
}
@@ -184,6 +187,7 @@ class RoutingSettingActivity : BaseActivity() {
return true
}
@SuppressLint("NotifyDataSetChanged")
fun refreshData() {
rulesets.clear()
rulesets.addAll(MmkvManager.decodeRoutingRulesets() ?: mutableListOf())

View File

@@ -6,6 +6,8 @@ import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContracts
import com.v2ray.ang.R
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastError
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.AngConfigManager
class ScScannerActivity : BaseActivity() {
@@ -27,7 +29,7 @@ class ScScannerActivity : BaseActivity() {
importQRcode()
}
fun importQRcode(): Boolean {
private fun importQRcode(): Boolean {
requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
return true
}
@@ -38,9 +40,9 @@ class ScScannerActivity : BaseActivity() {
val (count, countSub) = AngConfigManager.importBatchConfig(scanResult, "", false)
if (count + countSub > 0) {
toast(R.string.toast_success)
toastSuccess(R.string.toast_success)
} else {
toast(R.string.toast_failure)
toastError(R.string.toast_failure)
}
startActivity(Intent(this, MainActivity::class.java))

View File

@@ -6,6 +6,7 @@ import android.content.pm.PackageManager
import android.graphics.BitmapFactory
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import androidx.activity.result.contract.ActivityResultContracts
@@ -21,6 +22,7 @@ import io.github.g00fy2.quickie.config.ScannerConfig
class ScannerActivity : BaseActivity() {
private val scanQrCode = registerForActivityResult(ScanCustomCode(), ::handleResult)
private val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
val uri = it.data?.data
@@ -37,7 +39,7 @@ class ScannerActivity : BaseActivity() {
finished(text)
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to decode QR code from file", e)
toast(R.string.toast_decoding_failed)
}
}

View File

@@ -14,7 +14,6 @@ import android.widget.Spinner
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.AppConfig.DEFAULT_PORT
import com.v2ray.ang.AppConfig.PREF_ALLOW_INSECURE
import com.v2ray.ang.AppConfig.REALITY
@@ -28,6 +27,7 @@ import com.v2ray.ang.dto.NetworkType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.Utils
@@ -354,11 +354,11 @@ class ServerActivity : BaseActivity() {
container_alpn?.visibility = View.VISIBLE
et_sni?.text = Utils.getEditable(config.sni)
config.fingerPrint?.let {
config.fingerPrint?.let { it ->
val utlsIndex = Utils.arrayFind(uTlsItems, it)
utlsIndex.let { sp_stream_fingerprint?.setSelection(if (it >= 0) it else 0) }
}
config.alpn?.let {
config.alpn?.let { it ->
val alpnIndex = Utils.arrayFind(alpns, it)
alpnIndex.let { sp_stream_alpn?.setSelection(if (it >= 0) it else 0) }
}
@@ -480,9 +480,9 @@ class ServerActivity : BaseActivity() {
if (config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) {
config.subscriptionId = subscriptionId.orEmpty()
}
Log.d(ANG_PACKAGE, JsonUtil.toJsonPretty(config) ?: "")
//Log.i(AppConfig.TAG, JsonUtil.toJsonPretty(config) ?: "")
MmkvManager.encodeServerConfig(editGuid, config)
toast(R.string.toast_success)
toastSuccess(R.string.toast_success)
finish()
return true
}

View File

@@ -2,21 +2,22 @@ package com.v2ray.ang.ui
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import android.view.Menu
import android.view.MenuItem
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.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityServerCustomConfigBinding
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.fmt.CustomFmt
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.Utils
import me.drakeet.support.toast.ToastCompat
class ServerCustomConfigActivity : BaseActivity() {
private val binding by lazy { ActivityServerCustomConfigBinding.inflate(layoutInflater) }
@@ -77,7 +78,7 @@ class ServerCustomConfigActivity : BaseActivity() {
val profileItem = try {
CustomFmt.parse(binding.editor.text.toString())
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to parse custom configuration", e)
toast("${getString(R.string.toast_malformed_josn)} ${e.cause?.message}")
return false
}
@@ -91,7 +92,7 @@ class ServerCustomConfigActivity : BaseActivity() {
MmkvManager.encodeServerConfig(editGuid, config)
MmkvManager.encodeServerRaw(editGuid, binding.editor.text.toString())
toast(R.string.toast_success)
toastSuccess(R.string.toast_success)
finish()
return true
}

View File

@@ -10,6 +10,7 @@ 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.extension.toastSuccess
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.Utils
import kotlinx.coroutines.Dispatchers
@@ -18,8 +19,8 @@ import kotlinx.coroutines.launch
class SubEditActivity : BaseActivity() {
private val binding by lazy { ActivitySubEditBinding.inflate(layoutInflater) }
var del_config: MenuItem? = null
var save_config: MenuItem? = null
private var del_config: MenuItem? = null
private var save_config: MenuItem? = null
private val editSubId by lazy { intent.getStringExtra("subId").orEmpty() }
@@ -37,7 +38,7 @@ class SubEditActivity : BaseActivity() {
}
/**
* bingding seleced server config
* binding selected server config
*/
private fun bindingServer(subItem: SubscriptionItem): Boolean {
binding.etRemarks.text = Utils.getEditable(subItem.remarks)
@@ -94,7 +95,7 @@ class SubEditActivity : BaseActivity() {
}
MmkvManager.encodeSubscription(editSubId, subItem)
toast(R.string.toast_success)
toastSuccess(R.string.toast_success)
finish()
return true
}

View File

@@ -1,5 +1,6 @@
package com.v2ray.ang.ui
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.view.Menu
@@ -10,7 +11,8 @@ import androidx.recyclerview.widget.LinearLayoutManager
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivitySubSettingBinding
import com.v2ray.ang.dto.SubscriptionItem
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastError
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.AngConfigManager
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
@@ -64,9 +66,9 @@ class SubSettingActivity : BaseActivity() {
delay(500L)
launch(Dispatchers.Main) {
if (count > 0) {
toast(R.string.toast_success)
toastSuccess(R.string.toast_success)
} else {
toast(R.string.toast_failure)
toastError(R.string.toast_failure)
}
binding.pbWaiting.hide()
}
@@ -79,6 +81,7 @@ class SubSettingActivity : BaseActivity() {
}
@SuppressLint("NotifyDataSetChanged")
fun refreshData() {
subscriptions = MmkvManager.decodeSubscriptions()
adapter.notifyDataSetChanged()

View File

@@ -3,11 +3,13 @@ package com.v2ray.ang.ui
import android.content.Intent
import android.graphics.Color
import android.text.TextUtils
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.RecyclerView
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ItemQrcodeBinding
import com.v2ray.ang.databinding.ItemRecyclerSubSettingBinding
@@ -81,7 +83,7 @@ class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView
else -> mActivity.toast("else")
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Share subscription failed", e)
}
}.show()
}

View File

@@ -3,6 +3,7 @@ package com.v2ray.ang.ui
import android.content.Intent
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
@@ -28,7 +29,7 @@ class TaskerActivity : BaseActivity() {
lstData.add("Default")
lstGuid.add(AppConfig.TASKER_DEFAULT_GUID)
MmkvManager.decodeServerList()?.forEach { key ->
MmkvManager.decodeServerList().forEach { key ->
MmkvManager.decodeServerConfig(key)?.let { config ->
lstData.add(config.remarks)
lstGuid.add(key)
@@ -60,7 +61,7 @@ class TaskerActivity : BaseActivity() {
}
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to initialize Tasker settings", e)
}
}

View File

@@ -5,9 +5,11 @@ import android.net.Uri
import android.os.Bundle
import android.util.Log
import androidx.lifecycle.lifecycleScope
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityLogcatBinding
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastError
import com.v2ray.ang.handler.AngConfigManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -44,7 +46,7 @@ class UrlSchemeActivity : BaseActivity() {
}
else -> {
toast(R.string.toast_failure)
toastError(R.string.toast_failure)
}
}
}
@@ -53,7 +55,7 @@ class UrlSchemeActivity : BaseActivity() {
startActivity(Intent(this, MainActivity::class.java))
finish()
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Error processing URL scheme", e)
}
}
@@ -61,7 +63,7 @@ class UrlSchemeActivity : BaseActivity() {
if (uriString.isNullOrEmpty()) {
return
}
Log.d("UrlScheme", uriString)
Log.i(AppConfig.TAG, uriString)
var decodedUrl = URLDecoder.decode(uriString, "UTF-8")
val uri = Uri.parse(decodedUrl)
@@ -69,7 +71,7 @@ class UrlSchemeActivity : BaseActivity() {
if (uri.fragment.isNullOrEmpty() && !fragment.isNullOrEmpty()) {
decodedUrl += "#${fragment}"
}
Log.d("UrlScheme-decodedUrl", decodedUrl)
Log.i(AppConfig.TAG, decodedUrl)
lifecycleScope.launch(Dispatchers.IO) {
val (count, countSub) = AngConfigManager.importBatchConfig(decodedUrl, "", false)
withContext(Dispatchers.Main) {

View File

@@ -26,6 +26,8 @@ import com.v2ray.ang.databinding.ItemRecyclerUserAssetBinding
import com.v2ray.ang.dto.AssetUrlItem
import com.v2ray.ang.extension.toTrafficString
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastError
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.util.HttpUtil
@@ -42,6 +44,7 @@ import java.util.Date
class UserAssetActivity : BaseActivity() {
private val binding by lazy { ActivitySubSettingBinding.inflate(layoutInflater) }
val extDir by lazy { File(Utils.userAssetPath(this)) }
val builtInGeoFiles = arrayOf("geosite.dat", "geoip.dat")
@@ -91,7 +94,7 @@ class UserAssetActivity : BaseActivity() {
override fun onResume() {
super.onResume()
binding.recyclerView.adapter?.notifyDataSetChanged()
refreshData()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
@@ -117,7 +120,7 @@ class UserAssetActivity : BaseActivity() {
requestStoragePermissionLauncher.launch(permission)
}
val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
private val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val uri = result.data?.data
if (result.resultCode == RESULT_OK && uri != null) {
val assetId = Utils.getUuid()
@@ -135,7 +138,7 @@ class UserAssetActivity : BaseActivity() {
copyFile(uri)
}
}.onFailure {
toast(R.string.toast_asset_copy_failed)
toastError(R.string.toast_asset_copy_failed)
MmkvManager.removeAssetUrl(assetId)
}
}
@@ -146,8 +149,8 @@ class UserAssetActivity : BaseActivity() {
contentResolver.openInputStream(uri).use { inputStream ->
targetFile.outputStream().use { fileOut ->
inputStream?.copyTo(fileOut)
toast(R.string.toast_success)
binding.recyclerView.adapter?.notifyDataSetChanged()
toastSuccess(R.string.toast_success)
refreshData()
}
}
return targetFile.path
@@ -161,7 +164,7 @@ class UserAssetActivity : BaseActivity() {
}.also { cursor.close() }
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to get cursor name", e)
null
}
@@ -188,7 +191,7 @@ class UserAssetActivity : BaseActivity() {
.putExtra(UserAssetUrlActivity.ASSET_URL_QRCODE, url)
)
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to import asset from URL", e)
return false
}
return true
@@ -205,17 +208,21 @@ class UserAssetActivity : BaseActivity() {
var resultCount = 0
lifecycleScope.launch(Dispatchers.IO) {
assets.forEach {
var result = downloadGeo(it.second, 15000, httpPort)
if (!result) {
result = downloadGeo(it.second, 15000, 0)
try {
var result = downloadGeo(it.second, 15000, httpPort)
if (!result) {
result = downloadGeo(it.second, 15000, 0)
}
if (result)
resultCount++
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to download geo file: ${it.second.remarks}", e)
}
if (result)
resultCount++
}
withContext(Dispatchers.Main) {
if (resultCount > 0) {
toast(getString(R.string.title_update_config_count, resultCount))
binding.recyclerView.adapter?.notifyDataSetChanged()
refreshData()
} else {
toast(getString(R.string.toast_failure))
}
@@ -227,7 +234,7 @@ class UserAssetActivity : BaseActivity() {
private fun downloadGeo(item: AssetUrlItem, timeout: Int, httpPort: Int): Boolean {
val targetTemp = File(extDir, item.remarks + "_temp")
val target = File(extDir, item.remarks)
//Log.d(AppConfig.ANG_PACKAGE, url)
Log.i(AppConfig.TAG, "Downloading geo file: ${item.remarks} from ${item.url}")
val conn = HttpUtil.createProxyConnection(item.url, httpPort, timeout, timeout, needStream = true) ?: return false
try {
@@ -242,10 +249,10 @@ class UserAssetActivity : BaseActivity() {
}
return true
} catch (e: Exception) {
Log.e(AppConfig.ANG_PACKAGE, Log.getStackTraceString(e))
Log.e(AppConfig.TAG, "Failed to download geo file: ${item.remarks}", e)
return false
} finally {
conn?.disconnect()
conn.disconnect()
}
}
@@ -269,11 +276,16 @@ class UserAssetActivity : BaseActivity() {
lifecycleScope.launch(Dispatchers.Default) {
SettingsManager.initAssets(this@UserAssetActivity, assets)
withContext(Dispatchers.Main) {
binding.recyclerView.adapter?.notifyDataSetChanged()
refreshData()
}
}
}
@SuppressLint("NotifyDataSetChanged")
fun refreshData() {
binding.recyclerView.adapter?.notifyDataSetChanged()
}
inner class UserAssetAdapter : RecyclerView.Adapter<UserAssetViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserAssetViewHolder {
return UserAssetViewHolder(

View File

@@ -2,13 +2,16 @@ package com.v2ray.ang.ui
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AlertDialog
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityUserAssetUrlBinding
import com.v2ray.ang.dto.AssetUrlItem
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.Utils
import java.io.File
@@ -21,10 +24,10 @@ class UserAssetUrlActivity : BaseActivity() {
private val binding by lazy { ActivityUserAssetUrlBinding.inflate(layoutInflater) }
var del_config: MenuItem? = null
var save_config: MenuItem? = null
private var del_config: MenuItem? = null
private var save_config: MenuItem? = null
val extDir by lazy { File(Utils.userAssetPath(this)) }
private val extDir by lazy { File(Utils.userAssetPath(this)) }
private val editAssetId by lazy { intent.getStringExtra("assetId").orEmpty() }
override fun onCreate(savedInstanceState: Bundle?) {
@@ -41,6 +44,7 @@ class UserAssetUrlActivity : BaseActivity() {
binding.etRemarks.setText(assetNameQrcode)
binding.etUrl.setText(assetUrlQrcode)
}
else -> clearAsset()
}
}
@@ -73,7 +77,11 @@ class UserAssetUrlActivity : BaseActivity() {
// remove file associated with the asset
val file = extDir.resolve(assetItem.remarks)
if (file.exists()) {
file.delete()
try {
file.delete()
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to delete asset file: ${file.path}", e)
}
}
} else {
assetId = Utils.getUuid()
@@ -101,7 +109,7 @@ class UserAssetUrlActivity : BaseActivity() {
}
MmkvManager.encodeAsset(assetId, assetItem)
toast(R.string.toast_success)
toastSuccess(R.string.toast_success)
finish()
return true
}

View File

@@ -1,5 +1,7 @@
package com.v2ray.ang.util
import android.util.Log
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.LOOPBACK
import com.v2ray.ang.BuildConfig
import com.v2ray.ang.util.Utils.encode
@@ -10,6 +12,7 @@ import java.util.*
object HttpUtil {
/**
* Converts a URL string to its ASCII representation.
*
@@ -142,7 +145,7 @@ object HttpUtil {
)
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to create proxy connection", e)
// If an exception occurs, close the connection and return null
conn?.disconnect()
return null

View File

@@ -1,5 +1,6 @@
package com.v2ray.ang.util
import android.util.Log
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonObject
@@ -8,6 +9,7 @@ import com.google.gson.JsonPrimitive
import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer
import com.google.gson.reflect.TypeToken
import com.v2ray.ang.AppConfig
import java.lang.reflect.Type
object JsonUtil {
@@ -70,7 +72,7 @@ object JsonUtil {
try {
return JsonParser.parseString(src).getAsJsonObject()
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to parse JSON string", e)
return null
}
}

View File

@@ -3,12 +3,14 @@ package com.v2ray.ang.util
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.util.Log
import com.v2ray.ang.AppConfig
import com.v2ray.ang.service.V2RayTestService
import java.io.Serializable
object MessageUtil {
/**
* Sends a message to the service.
*
@@ -46,7 +48,7 @@ object MessageUtil {
intent.putExtra("content", content)
ctx.startService(intent)
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to send message to test service", e)
}
}
@@ -67,7 +69,7 @@ object MessageUtil {
intent.putExtra("content", content)
ctx.sendBroadcast(intent)
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to send message with action: $action", e)
}
}
}

View File

@@ -3,7 +3,7 @@ package com.v2ray.ang.util
import android.content.Context
import android.os.SystemClock
import android.util.Log
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.fmt.Hysteria2Fmt
@@ -13,7 +13,7 @@ import java.io.File
object PluginUtil {
private const val HYSTERIA2 = "libhysteria2.so"
private const val TAG = ANG_PACKAGE
private val procService: ProcessService by lazy {
ProcessService()
}
@@ -26,13 +26,23 @@ object PluginUtil {
* @param domainPort The domain and port information.
*/
fun runPlugin(context: Context, config: ProfileItem?, domainPort: String?) {
Log.d(TAG, "runPlugin")
Log.i(AppConfig.TAG, "Starting plugin execution")
if (config?.configType?.equals(EConfigType.HYSTERIA2) == true) {
val configFile = genConfigHy2(context, config, domainPort) ?: return
val cmd = genCmdHy2(context, configFile)
if (config == null) {
Log.w(AppConfig.TAG, "Cannot run plugin: config is null")
return
}
try {
if (config.configType == EConfigType.HYSTERIA2) {
Log.i(AppConfig.TAG, "Running Hysteria2 plugin")
val configFile = genConfigHy2(context, config, domainPort) ?: return
val cmd = genCmdHy2(context, configFile)
procService.runProcess(context, cmd)
procService.runProcess(context, cmd)
}
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Error running plugin", e)
}
}
@@ -51,7 +61,7 @@ object PluginUtil {
* @return The ping delay in milliseconds, or -1 if it fails.
*/
fun realPingHy2(context: Context, config: ProfileItem?): Long {
Log.d(TAG, "realPingHy2")
Log.i(AppConfig.TAG, "realPingHy2")
val retFailure = -1L
if (config?.configType?.equals(EConfigType.HYSTERIA2) == true) {
@@ -79,18 +89,18 @@ object PluginUtil {
* @return The generated configuration file.
*/
private fun genConfigHy2(context: Context, config: ProfileItem, domainPort: String?): File? {
Log.d(TAG, "runPlugin $HYSTERIA2")
Log.i(AppConfig.TAG, "runPlugin $HYSTERIA2")
val socksPort = domainPort?.split(":")?.last()
.let { if (it.isNullOrEmpty()) return null else it.toInt() }
val hy2Config = Hysteria2Fmt.toNativeConfig(config, socksPort) ?: return null
val configFile = File(context.noBackupFilesDir, "hy2_${SystemClock.elapsedRealtime()}.json")
Log.d(TAG, "runPlugin ${configFile.absolutePath}")
Log.i(AppConfig.TAG, "runPlugin ${configFile.absolutePath}")
configFile.parentFile?.mkdirs()
configFile.writeText(JsonUtil.toJson(hy2Config))
Log.d(TAG, JsonUtil.toJson(hy2Config))
Log.i(AppConfig.TAG, JsonUtil.toJson(hy2Config))
return configFile
}
@@ -119,10 +129,10 @@ object PluginUtil {
*/
private fun stopHy2() {
try {
Log.d(TAG, "$HYSTERIA2 destroy")
Log.i(AppConfig.TAG, "$HYSTERIA2 destroy")
procService?.stopProcess()
} catch (e: Exception) {
Log.d(TAG, e.toString())
Log.e(AppConfig.TAG, "Failed to stop Hysteria2 process", e)
}
}
}

View File

@@ -28,6 +28,10 @@ import java.util.UUID
object Utils {
private val IPV4_REGEX =
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])$")
private val IPV6_REGEX = 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}$")
/**
* Convert string to editable for Kotlin.
*
@@ -46,22 +50,7 @@ object Utils {
* @return The index of the value in the array, or -1 if not found.
*/
fun arrayFind(array: Array<out String>, value: String): Int {
for (i in array.indices) {
if (array[i] == value) {
return i
}
}
return -1
}
/**
* Parse a string to an integer.
*
* @param str The string to parse.
* @return The parsed integer, or 0 if parsing fails.
*/
fun parseInt(str: String): Int {
return parseInt(str, 0)
return array.indexOf(value)
}
/**
@@ -71,7 +60,7 @@ object Utils {
* @param default The default value if parsing fails.
* @return The parsed integer, or the default value if parsing fails.
*/
fun parseInt(str: String?, default: Int): Int {
fun parseInt(str: String?, default: Int = 0): Int {
return str?.toIntOrNull() ?: default
}
@@ -86,7 +75,7 @@ object Utils {
val cmb = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
cmb.primaryClip?.getItemAt(0)?.text.toString()
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to get clipboard content", e)
""
}
}
@@ -103,7 +92,7 @@ object Utils {
val clipData = ClipData.newPlainText(null, content)
cmb.setPrimaryClip(clipData)
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to set clipboard content", e)
}
}
@@ -124,15 +113,17 @@ object Utils {
* @return The decoded string, or null if decoding fails.
*/
fun tryDecodeBase64(text: String?): String? {
if (text.isNullOrEmpty()) return null
try {
return Base64.decode(text, Base64.NO_WRAP).toString(Charsets.UTF_8)
} catch (e: Exception) {
Log.i(ANG_PACKAGE, "Parse base64 standard failed $e")
Log.e(AppConfig.TAG, "Failed to decode standard base64", e)
}
try {
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")
Log.e(AppConfig.TAG, "Failed to decode URL-safe base64", e)
}
return null
}
@@ -147,7 +138,7 @@ object Utils {
return try {
Base64.encodeToString(text.toByteArray(Charsets.UTF_8), Base64.NO_WRAP)
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to encode text to base64", e)
""
}
}
@@ -159,43 +150,38 @@ object Utils {
* @return True if the string is a valid IP address, false otherwise.
*/
fun isIpAddress(value: String?): Boolean {
if (value.isNullOrEmpty()) return false
try {
if (value.isNullOrEmpty()) {
return false
}
var addr = value
if (addr.isEmpty() || addr.isBlank()) {
return false
}
var addr = value.trim()
if (addr.isEmpty()) return false
//CIDR
if (addr.indexOf("/") > 0) {
if (addr.contains("/")) {
val arr = addr.split("/")
if (arr.count() == 2 && Integer.parseInt(arr[1]) > -1) {
if (arr.size == 2 && arr[1].toIntOrNull() != null && arr[1].toInt() > -1) {
addr = arr[0]
}
}
// "::ffff:192.168.173.22"
// "[::ffff:192.168.173.22]:80"
// Handle IPv4-mapped IPv6 addresses
if (addr.startsWith("::ffff:") && '.' in addr) {
addr = addr.drop(7)
} else if (addr.startsWith("[::ffff:") && '.' in addr) {
addr = addr.drop(8).replace("]", "")
}
// addr = addr.toLowerCase()
val octets = addr.split('.').toTypedArray()
val octets = addr.split('.')
if (octets.size == 4) {
if (octets[3].indexOf(":") > 0) {
if (octets[3].contains(":")) {
addr = addr.substring(0, addr.indexOf(":"))
}
return isIpv4Address(addr)
}
// Ipv6addr [2001:abc::123]:8080
return isIpv6Address(addr)
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to validate IP address", e)
return false
}
}
@@ -217,9 +203,7 @@ object Utils {
* @return True if the string is a valid IPv4 address, false otherwise.
*/
private 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])$")
return regV4.matches(value)
return IPV4_REGEX.matches(value)
}
/**
@@ -230,13 +214,10 @@ object Utils {
*/
private fun isIpv6Address(value: String): Boolean {
var addr = value
if (addr.indexOf("[") == 0 && addr.lastIndexOf("]") > 0) {
addr = addr.drop(1)
addr = addr.dropLast(addr.count() - addr.lastIndexOf("]"))
if (addr.startsWith("[") && addr.endsWith("]")) {
addr = addr.drop(1).dropLast(1)
}
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)
return IPV6_REGEX.matches(addr)
}
/**
@@ -246,10 +227,10 @@ object Utils {
* @return True if the string is a CoreDNS address, false otherwise.
*/
fun isCoreDNSAddress(s: String): Boolean {
return s.startsWith("https")
|| s.startsWith("tcp")
|| s.startsWith("quic")
|| s == "localhost"
return s.startsWith("https") ||
s.startsWith("tcp") ||
s.startsWith("quic") ||
s == "localhost"
}
/**
@@ -259,21 +240,16 @@ object Utils {
* @return True if the string is a valid URL, false otherwise.
*/
fun isValidUrl(value: String?): Boolean {
try {
if (value.isNullOrEmpty()) {
return false
}
if (Patterns.WEB_URL.matcher(value).matches()
|| Patterns.DOMAIN_NAME.matcher(value).matches()
|| URLUtil.isValidUrl(value)
) {
return true
}
if (value.isNullOrEmpty()) return false
return try {
Patterns.WEB_URL.matcher(value).matches() ||
Patterns.DOMAIN_NAME.matcher(value).matches() ||
URLUtil.isValidUrl(value)
} catch (e: Exception) {
e.printStackTrace()
return false
Log.e(AppConfig.TAG, "Failed to validate URL", e)
false
}
return false
}
/**
@@ -283,8 +259,12 @@ object Utils {
* @param uriString The URI string to open.
*/
fun openUri(context: Context, uriString: String) {
val uri = uriString.toUri()
context.startActivity(Intent(Intent.ACTION_VIEW, uri))
try {
val uri = uriString.toUri()
context.startActivity(Intent(Intent.ACTION_VIEW, uri))
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to open URI", e)
}
}
/**
@@ -296,7 +276,7 @@ object Utils {
return try {
UUID.randomUUID().toString().replace("-", "")
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to generate UUID", e)
""
}
}
@@ -311,7 +291,7 @@ object Utils {
return try {
URLDecoder.decode(url, Charsets.UTF_8.toString())
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to decode URL", e)
url
}
}
@@ -326,7 +306,7 @@ object Utils {
return try {
URLEncoder.encode(url, Charsets.UTF_8.toString()).replace("+", "%20")
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to encode URL", e)
url
}
}
@@ -339,13 +319,18 @@ object Utils {
* @return The content of the asset file as a string.
*/
fun readTextFromAssets(context: Context?, fileName: String): String {
if (context == null) {
return ""
if (context == null) return ""
return try {
context.assets.open(fileName).use { inputStream ->
inputStream.bufferedReader().use { reader ->
reader.readText()
}
}
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to read asset file: $fileName", e)
""
}
val content = context.assets.open(fileName).bufferedReader().use {
it.readText()
}
return content
}
/**
@@ -355,11 +340,15 @@ object Utils {
* @return The path to the user asset directory.
*/
fun userAssetPath(context: Context?): String {
if (context == null)
return ""
val extDir = context.getExternalFilesDir(AppConfig.DIR_ASSETS)
?: return context.getDir(AppConfig.DIR_ASSETS, 0).absolutePath
return extDir.absolutePath
if (context == null) return ""
return try {
context.getExternalFilesDir(AppConfig.DIR_ASSETS)?.absolutePath
?: context.getDir(AppConfig.DIR_ASSETS, 0).absolutePath
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to get user asset path", e)
""
}
}
/**
@@ -369,11 +358,15 @@ object Utils {
* @return The path to the backup directory.
*/
fun backupPath(context: Context?): String {
if (context == null)
return ""
val extDir = context.getExternalFilesDir(AppConfig.DIR_BACKUPS)
?: return context.getDir(AppConfig.DIR_BACKUPS, 0).absolutePath
return extDir.absolutePath
if (context == null) return ""
return try {
context.getExternalFilesDir(AppConfig.DIR_BACKUPS)?.absolutePath
?: context.getDir(AppConfig.DIR_BACKUPS, 0).absolutePath
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to get backup path", e)
""
}
}
/**
@@ -382,8 +375,13 @@ object Utils {
* @return The device ID for XUDP base key.
*/
fun getDeviceIdForXUDPBaseKey(): String {
val androidId = Settings.Secure.ANDROID_ID.toByteArray(Charsets.UTF_8)
return Base64.encodeToString(androidId.copyOf(32), Base64.NO_PADDING.or(Base64.URL_SAFE))
return try {
val androidId = Settings.Secure.ANDROID_ID.toByteArray(Charsets.UTF_8)
Base64.encodeToString(androidId.copyOf(32), Base64.NO_PADDING.or(Base64.URL_SAFE))
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to generate device ID", e)
""
}
}
/**
@@ -403,11 +401,10 @@ object Utils {
* @return The formatted IPv6 address, or the original address if not valid.
*/
fun getIpv6Address(address: String?): String {
if (address == null) {
return ""
}
if (address.isNullOrEmpty()) return ""
return if (isIpv6Address(address) && !address.contains('[') && !address.contains(']')) {
String.format("[%s]", address)
"[$address]"
} else {
address
}
@@ -431,8 +428,7 @@ object Utils {
* @return The URL string with illegal characters replaced.
*/
fun fixIllegalUrl(str: String): String {
return str
.replace(" ", "%20")
return str.replace(" ", "%20")
.replace("|", "%7C")
}
@@ -463,14 +459,15 @@ object Utils {
* @return True if the string is a valid subscription URL, false otherwise.
*/
fun isValidSubUrl(value: String?): Boolean {
try {
if (value.isNullOrEmpty()) return false
if (URLUtil.isHttpsUrl(value)) return true
if (URLUtil.isHttpUrl(value) && value.contains(LOOPBACK)) return true
if (value.isNullOrEmpty()) return false
return try {
URLUtil.isHttpsUrl(value) ||
(URLUtil.isHttpUrl(value) && value.contains(LOOPBACK))
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to validate subscription URL", e)
false
}
return false
}
/**
@@ -489,7 +486,7 @@ object Utils {
*
* @return True if the package is Xray, false otherwise.
*/
fun isXray(): Boolean = (ANG_PACKAGE.startsWith("com.v2ray.ang"))
fun isXray(): Boolean = ANG_PACKAGE.startsWith("com.v2ray.ang")
}

View File

@@ -1,5 +1,7 @@
package com.v2ray.ang.util
import android.util.Log
import com.v2ray.ang.AppConfig
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileInputStream
@@ -61,7 +63,7 @@ object ZipUtil {
zos.closeEntry()
zos.close()
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to zip folder", e)
return false
}
return true
@@ -97,7 +99,7 @@ object ZipUtil {
}
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to unzip file", e)
return false
}
return true

View File

@@ -13,12 +13,12 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
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.dto.ProfileItem
import com.v2ray.ang.dto.ServersCache
import com.v2ray.ang.extension.serializable
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastError
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.AngConfigManager
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsManager
@@ -62,7 +62,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
getApplication<AngApplication>().unregisterReceiver(mMsgReceiver)
tcpingTestScope.coroutineContext[Job]?.cancelChildren()
SpeedtestManager.closeAllTcpSockets()
Log.i(ANG_PACKAGE, "Main ViewModel is cleared")
Log.i(AppConfig.TAG, "Main ViewModel is cleared")
super.onCleared()
}
@@ -419,12 +419,12 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
}
AppConfig.MSG_STATE_START_SUCCESS -> {
getApplication<AngApplication>().toast(R.string.toast_services_success)
getApplication<AngApplication>().toastSuccess(R.string.toast_services_success)
isRunning.value = true
}
AppConfig.MSG_STATE_START_FAILURE -> {
getApplication<AngApplication>().toast(R.string.toast_services_failure)
getApplication<AngApplication>().toastError(R.string.toast_services_failure)
isRunning.value = false
}

View File

@@ -26,7 +26,7 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
override fun onCleared() {
PreferenceManager.getDefaultSharedPreferences(getApplication())
.unregisterOnSharedPreferenceChangeListener(this)
Log.i(AppConfig.ANG_PACKAGE, "Settings ViewModel is cleared")
Log.i(AppConfig.TAG, "Settings ViewModel is cleared")
super.onCleared()
}
@@ -36,7 +36,7 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
* @param key The key of the changed preference.
*/
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
Log.d(AppConfig.ANG_PACKAGE, "Observe settings changed: $key")
Log.i(AppConfig.TAG, "Observe settings changed: $key")
when (key) {
AppConfig.PREF_MODE,
AppConfig.PREF_VPN_DNS,

View File

@@ -4,6 +4,6 @@
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M537,85.3A85.3,85.3 0,0 1,597.3 110.3L828.3,341.3A85.3,85.3 0,0 1,853.3 401.7L853.3,810.7a128,128 0,0 1,-128 128L298.7,938.7a128,128 0,0 1,-128 -128L170.7,213.3a128,128 0,0 1,128 -128zM537,170.7L298.7,170.7a42.7,42.7 0,0 0,-42.7 42.7v597.3a42.7,42.7 0,0 0,42.7 42.7h426.7a42.7,42.7 0,0 0,42.7 -42.7L768,401.7L537,170.7zM512,384a42.7,42.7 0,0 1,42.4 37.7L554.7,426.7v85.3h85.3a42.7,42.7 0,0 1,5 85L640,597.3h-85.3v85.3a42.7,42.7 0,0 1,-85 5L469.3,682.7v-85.3L384,597.3a42.7,42.7 0,0 1,-5 -85L384,512h85.3v-85.3a42.7,42.7 0,0 1,42.7 -42.7z"
android:fillColor="#FFFFFFFF" />
android:fillColor="#FFFFFFFF"
android:pathData="M537,85.3A85.3,85.3 0,0 1,597.3 110.3L828.3,341.3A85.3,85.3 0,0 1,853.3 401.7L853.3,810.7a128,128 0,0 1,-128 128L298.7,938.7a128,128 0,0 1,-128 -128L170.7,213.3a128,128 0,0 1,128 -128zM537,170.7L298.7,170.7a42.7,42.7 0,0 0,-42.7 42.7v597.3a42.7,42.7 0,0 0,42.7 42.7h426.7a42.7,42.7 0,0 0,42.7 -42.7L768,401.7L537,170.7zM512,384a42.7,42.7 0,0 1,42.4 37.7L554.7,426.7v85.3h85.3a42.7,42.7 0,0 1,5 85L640,597.3h-85.3v85.3a42.7,42.7 0,0 1,-85 5L469.3,682.7v-85.3L384,597.3a42.7,42.7 0,0 1,-5 -85L384,512h85.3v-85.3a42.7,42.7 0,0 1,42.7 -42.7z" />
</vector>

View File

@@ -4,12 +4,12 @@
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M273.2,212.8h583.7L856.9,906.2h-583.7L273.2,212.8zM344.9,284.5L344.9,834.6h440.3L785.2,284.5h-440.3z"
android:fillColor="#FFFFFFFF" />
android:fillColor="#FFFFFFFF"
android:pathData="M273.2,212.8h583.7L856.9,906.2h-583.7L273.2,212.8zM344.9,284.5L344.9,834.6h440.3L785.2,284.5h-440.3z" />
<path
android:pathData="M167.1,117.8h554.4v123.6h-71.7V189.4H238.8v498.8h73.9v71.7H167.1V117.8z"
android:fillColor="#FFFFFFFF" />
android:fillColor="#FFFFFFFF"
android:pathData="M167.1,117.8h554.4v123.6h-71.7V189.4H238.8v498.8h73.9v71.7H167.1V117.8z" />
<path
android:pathData="M674.8,504H455.3v-71.7h219.4v71.7zM674.8,650.2H455.3v-71.7h219.4v71.7z"
android:fillColor="#FFFFFFFF" />
android:fillColor="#FFFFFFFF"
android:pathData="M674.8,504H455.3v-71.7h219.4v71.7zM674.8,650.2H455.3v-71.7h219.4v71.7z" />
</vector>

View File

@@ -1,9 +1,9 @@
<vector android:height="24dp"
android:tint="#FFFFFF"
android:viewportHeight="24"
android:viewportWidth="24"
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<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" />

View File

@@ -4,9 +4,9 @@
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M723.9,312c-13.2,-13.2 -34.7,-13.2 -47.9,0 -6.6,6.6 -9.9,15.3 -9.9,23.9 0,8.7 3.3,17.3 9.9,23.9 65.9,65.9 80.9,165.5 37.3,247.8 -4.9,9.2 -10.5,18.2 -16.9,26.8 -6.4,8.7 -12.9,16.4 -20,23.3 -13.4,13.1 -13.7,34.5 -0.6,47.9 13.1,13.4 34.5,13.7 47.9,0.6 9.7,-9.5 18.9,-20.2 27.3,-31.6 8.3,-11.2 15.7,-23 22.2,-35.2 57.5,-108.7 37.7,-240.3 -49.3,-327.4zM507.6,470c-0.2,-0.2 -0.4,-0.3 -0.7,-0.5 -0.8,-0.3 -1.7,-0.1 -2.3,0.5 -0.2,0.2 -0.3,0.4 -0.5,0.7 -0.1,0.3 -0.2,0.5 -0.2,0.8 0,0.6 0.3,1.1 0.6,1.5 0.2,0.2 0.4,0.3 0.7,0.5 0.3,0.1 0.5,0.2 0.8,0.2 0.6,0 1.1,-0.3 1.5,-0.6 0.4,-0.4 0.6,-1 0.6,-1.5 0,-0.3 -0.1,-0.5 -0.2,-0.8 0,-0.4 -0.2,-0.6 -0.3,-0.8z"
android:fillColor="#FFFFFFFF" />
android:fillColor="#FFFFFFFF"
android:pathData="M723.9,312c-13.2,-13.2 -34.7,-13.2 -47.9,0 -6.6,6.6 -9.9,15.3 -9.9,23.9 0,8.7 3.3,17.3 9.9,23.9 65.9,65.9 80.9,165.5 37.3,247.8 -4.9,9.2 -10.5,18.2 -16.9,26.8 -6.4,8.7 -12.9,16.4 -20,23.3 -13.4,13.1 -13.7,34.5 -0.6,47.9 13.1,13.4 34.5,13.7 47.9,0.6 9.7,-9.5 18.9,-20.2 27.3,-31.6 8.3,-11.2 15.7,-23 22.2,-35.2 57.5,-108.7 37.7,-240.3 -49.3,-327.4zM507.6,470c-0.2,-0.2 -0.4,-0.3 -0.7,-0.5 -0.8,-0.3 -1.7,-0.1 -2.3,0.5 -0.2,0.2 -0.3,0.4 -0.5,0.7 -0.1,0.3 -0.2,0.5 -0.2,0.8 0,0.6 0.3,1.1 0.6,1.5 0.2,0.2 0.4,0.3 0.7,0.5 0.3,0.1 0.5,0.2 0.8,0.2 0.6,0 1.1,-0.3 1.5,-0.6 0.4,-0.4 0.6,-1 0.6,-1.5 0,-0.3 -0.1,-0.5 -0.2,-0.8 0,-0.4 -0.2,-0.6 -0.3,-0.8z" />
<path
android:pathData="M833.7,209.6c-13.2,-13.2 -34.5,-13.2 -47.6,0 -13.2,13.2 -13.2,34.5 0,47.6 122.3,122.3 140,314.4 42.1,456.6 -12.5,18.1 -26.5,35 -42.1,50.5 -6.6,6.6 -9.9,15.2 -9.9,23.8 0,8.6 3.3,17.2 9.9,23.8 13.2,13.2 34.5,13.2 47.6,0 18.4,-18.4 35.1,-38.5 49.9,-60C1000,583.1 979,355 833.7,209.6zM515.1,166.8c-48,-22.3 -102.9,-15.1 -143.4,19L176.6,349.9h-50.2c-31,0 -63.3,25.2 -63.3,56.3v209.4c0,31 32.2,56.3 63.3,56.3h50.2L371.7,836c24.9,21 55.4,31.8 86.2,31.8 19.3,0 38.7,-4.2 57.2,-12.8 48,-22.3 77.8,-69.1 77.8,-122L592.9,288.8c0,-52.9 -29.8,-99.6 -77.8,-122zM531.1,732.9c0,31.8 -17.2,52.9 -46.1,66.3 -28.9,13.4 -56.7,6.2 -81,-14.3L206.2,623.5c-4.9,-4.2 -11.2,-6.4 -17.6,-6.4h-60.2c-0.8,0 -1.5,-0.7 -1.5,-1.5L126.9,406.2c0,-0.8 0.7,-1.5 1.5,-1.5h60.2c6.5,0 12.7,-2.3 17.6,-6.4L412,237.7c24.4,-20.5 51.2,-24.7 80,-11.3 28.9,13.4 39.1,30.5 39.1,62.3v444.2z"
android:fillColor="#FFFFFFFF" />
android:fillColor="#FFFFFFFF"
android:pathData="M833.7,209.6c-13.2,-13.2 -34.5,-13.2 -47.6,0 -13.2,13.2 -13.2,34.5 0,47.6 122.3,122.3 140,314.4 42.1,456.6 -12.5,18.1 -26.5,35 -42.1,50.5 -6.6,6.6 -9.9,15.2 -9.9,23.8 0,8.6 3.3,17.2 9.9,23.8 13.2,13.2 34.5,13.2 47.6,0 18.4,-18.4 35.1,-38.5 49.9,-60C1000,583.1 979,355 833.7,209.6zM515.1,166.8c-48,-22.3 -102.9,-15.1 -143.4,19L176.6,349.9h-50.2c-31,0 -63.3,25.2 -63.3,56.3v209.4c0,31 32.2,56.3 63.3,56.3h50.2L371.7,836c24.9,21 55.4,31.8 86.2,31.8 19.3,0 38.7,-4.2 57.2,-12.8 48,-22.3 77.8,-69.1 77.8,-122L592.9,288.8c0,-52.9 -29.8,-99.6 -77.8,-122zM531.1,732.9c0,31.8 -17.2,52.9 -46.1,66.3 -28.9,13.4 -56.7,6.2 -81,-14.3L206.2,623.5c-4.9,-4.2 -11.2,-6.4 -17.6,-6.4h-60.2c-0.8,0 -1.5,-0.7 -1.5,-1.5L126.9,406.2c0,-0.8 0.7,-1.5 1.5,-1.5h60.2c6.5,0 12.7,-2.3 17.6,-6.4L412,237.7c24.4,-20.5 51.2,-24.7 80,-11.3 28.9,13.4 39.1,30.5 39.1,62.3v444.2z" />
</vector>

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0"
android:width="24dp">
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M3,5h2L5,3c-1.1,0 -2,0.9 -2,2zM3,13h2v-2L3,11v2zM7,21h2v-2L7,19v2zM3,9h2L5,7L3,7v2zM13,3h-2v2h2L13,3zM19,3v2h2c0,-1.1 -0.9,-2 -2,-2zM5,21v-2L3,19c0,1.1 0.9,2 2,2zM3,17h2v-2L3,15v2zM9,3L7,3v2h2L9,3zM11,21h2v-2h-2v2zM19,13h2v-2h-2v2zM19,21c1.1,0 2,-0.9 2,-2h-2v2zM19,9h2L21,7h-2v2zM19,17h2v-2h-2v2zM15,21h2v-2h-2v2zM15,5h2L17,3h-2v2zM7,17h10L17,7L7,7v10zM9,9h6v6L9,15L9,9z" />

View File

@@ -3,5 +3,5 @@
<solid android:color="@color/divider_color_light" />
<size
android:width="48dp"
android:height="48dp"/>
android:height="48dp" />
</shape>

View File

@@ -4,6 +4,6 @@
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M537,85.3A85.3,85.3 0,0 1,597.3 110.3L828.3,341.3A85.3,85.3 0,0 1,853.3 401.7L853.3,810.7a128,128 0,0 1,-128 128L298.7,938.7a128,128 0,0 1,-128 -128L170.7,213.3a128,128 0,0 1,128 -128zM537,170.7L298.7,170.7a42.7,42.7 0,0 0,-42.7 42.7v597.3a42.7,42.7 0,0 0,42.7 42.7h426.7a42.7,42.7 0,0 0,42.7 -42.7L768,401.7L537,170.7zM512,384a42.7,42.7 0,0 1,42.4 37.7L554.7,426.7v85.3h85.3a42.7,42.7 0,0 1,5 85L640,597.3h-85.3v85.3a42.7,42.7 0,0 1,-85 5L469.3,682.7v-85.3L384,597.3a42.7,42.7 0,0 1,-5 -85L384,512h85.3v-85.3a42.7,42.7 0,0 1,42.7 -42.7z"
android:fillColor="#FF000000" />
android:fillColor="#FF000000"
android:pathData="M537,85.3A85.3,85.3 0,0 1,597.3 110.3L828.3,341.3A85.3,85.3 0,0 1,853.3 401.7L853.3,810.7a128,128 0,0 1,-128 128L298.7,938.7a128,128 0,0 1,-128 -128L170.7,213.3a128,128 0,0 1,128 -128zM537,170.7L298.7,170.7a42.7,42.7 0,0 0,-42.7 42.7v597.3a42.7,42.7 0,0 0,42.7 42.7h426.7a42.7,42.7 0,0 0,42.7 -42.7L768,401.7L537,170.7zM512,384a42.7,42.7 0,0 1,42.4 37.7L554.7,426.7v85.3h85.3a42.7,42.7 0,0 1,5 85L640,597.3h-85.3v85.3a42.7,42.7 0,0 1,-85 5L469.3,682.7v-85.3L384,597.3a42.7,42.7 0,0 1,-5 -85L384,512h85.3v-85.3a42.7,42.7 0,0 1,42.7 -42.7z" />
</vector>

View File

@@ -4,12 +4,12 @@
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M273.2,212.8h583.7L856.9,906.2h-583.7L273.2,212.8zM344.9,284.5L344.9,834.6h440.3L785.2,284.5h-440.3z"
android:fillColor="#FF000000" />
android:fillColor="#FF000000"
android:pathData="M273.2,212.8h583.7L856.9,906.2h-583.7L273.2,212.8zM344.9,284.5L344.9,834.6h440.3L785.2,284.5h-440.3z" />
<path
android:pathData="M167.1,117.8h554.4v123.6h-71.7V189.4H238.8v498.8h73.9v71.7H167.1V117.8z"
android:fillColor="#FF000000" />
android:fillColor="#FF000000"
android:pathData="M167.1,117.8h554.4v123.6h-71.7V189.4H238.8v498.8h73.9v71.7H167.1V117.8z" />
<path
android:pathData="M674.8,504H455.3v-71.7h219.4v71.7zM674.8,650.2H455.3v-71.7h219.4v71.7z"
android:fillColor="#FF000000" />
android:fillColor="#FF000000"
android:pathData="M674.8,504H455.3v-71.7h219.4v71.7zM674.8,650.2H455.3v-71.7h219.4v71.7z" />
</vector>

View File

@@ -1,9 +1,9 @@
<vector android:height="24dp"
android:tint="#000000"
android:viewportHeight="24"
android:viewportWidth="24"
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
android:height="24dp"
android:tint="#000000"
android:viewportWidth="24"
android:viewportHeight="24">
<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" />

View File

@@ -4,9 +4,9 @@
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M723.9,312c-13.2,-13.2 -34.7,-13.2 -47.9,0 -6.6,6.6 -9.9,15.3 -9.9,23.9 0,8.7 3.3,17.3 9.9,23.9 65.9,65.9 80.9,165.5 37.3,247.8 -4.9,9.2 -10.5,18.2 -16.9,26.8 -6.4,8.7 -12.9,16.4 -20,23.3 -13.4,13.1 -13.7,34.5 -0.6,47.9 13.1,13.4 34.5,13.7 47.9,0.6 9.7,-9.5 18.9,-20.2 27.3,-31.6 8.3,-11.2 15.7,-23 22.2,-35.2 57.5,-108.7 37.7,-240.3 -49.3,-327.4zM507.6,470c-0.2,-0.2 -0.4,-0.3 -0.7,-0.5 -0.8,-0.3 -1.7,-0.1 -2.3,0.5 -0.2,0.2 -0.3,0.4 -0.5,0.7 -0.1,0.3 -0.2,0.5 -0.2,0.8 0,0.6 0.3,1.1 0.6,1.5 0.2,0.2 0.4,0.3 0.7,0.5 0.3,0.1 0.5,0.2 0.8,0.2 0.6,0 1.1,-0.3 1.5,-0.6 0.4,-0.4 0.6,-1 0.6,-1.5 0,-0.3 -0.1,-0.5 -0.2,-0.8 0,-0.4 -0.2,-0.6 -0.3,-0.8z"
android:fillColor="#FF000000" />
android:fillColor="#FF000000"
android:pathData="M723.9,312c-13.2,-13.2 -34.7,-13.2 -47.9,0 -6.6,6.6 -9.9,15.3 -9.9,23.9 0,8.7 3.3,17.3 9.9,23.9 65.9,65.9 80.9,165.5 37.3,247.8 -4.9,9.2 -10.5,18.2 -16.9,26.8 -6.4,8.7 -12.9,16.4 -20,23.3 -13.4,13.1 -13.7,34.5 -0.6,47.9 13.1,13.4 34.5,13.7 47.9,0.6 9.7,-9.5 18.9,-20.2 27.3,-31.6 8.3,-11.2 15.7,-23 22.2,-35.2 57.5,-108.7 37.7,-240.3 -49.3,-327.4zM507.6,470c-0.2,-0.2 -0.4,-0.3 -0.7,-0.5 -0.8,-0.3 -1.7,-0.1 -2.3,0.5 -0.2,0.2 -0.3,0.4 -0.5,0.7 -0.1,0.3 -0.2,0.5 -0.2,0.8 0,0.6 0.3,1.1 0.6,1.5 0.2,0.2 0.4,0.3 0.7,0.5 0.3,0.1 0.5,0.2 0.8,0.2 0.6,0 1.1,-0.3 1.5,-0.6 0.4,-0.4 0.6,-1 0.6,-1.5 0,-0.3 -0.1,-0.5 -0.2,-0.8 0,-0.4 -0.2,-0.6 -0.3,-0.8z" />
<path
android:pathData="M833.7,209.6c-13.2,-13.2 -34.5,-13.2 -47.6,0 -13.2,13.2 -13.2,34.5 0,47.6 122.3,122.3 140,314.4 42.1,456.6 -12.5,18.1 -26.5,35 -42.1,50.5 -6.6,6.6 -9.9,15.2 -9.9,23.8 0,8.6 3.3,17.2 9.9,23.8 13.2,13.2 34.5,13.2 47.6,0 18.4,-18.4 35.1,-38.5 49.9,-60C1000,583.1 979,355 833.7,209.6zM515.1,166.8c-48,-22.3 -102.9,-15.1 -143.4,19L176.6,349.9h-50.2c-31,0 -63.3,25.2 -63.3,56.3v209.4c0,31 32.2,56.3 63.3,56.3h50.2L371.7,836c24.9,21 55.4,31.8 86.2,31.8 19.3,0 38.7,-4.2 57.2,-12.8 48,-22.3 77.8,-69.1 77.8,-122L592.9,288.8c0,-52.9 -29.8,-99.6 -77.8,-122zM531.1,732.9c0,31.8 -17.2,52.9 -46.1,66.3 -28.9,13.4 -56.7,6.2 -81,-14.3L206.2,623.5c-4.9,-4.2 -11.2,-6.4 -17.6,-6.4h-60.2c-0.8,0 -1.5,-0.7 -1.5,-1.5L126.9,406.2c0,-0.8 0.7,-1.5 1.5,-1.5h60.2c6.5,0 12.7,-2.3 17.6,-6.4L412,237.7c24.4,-20.5 51.2,-24.7 80,-11.3 28.9,13.4 39.1,30.5 39.1,62.3v444.2z"
android:fillColor="#FF000000" />
android:fillColor="#FF000000"
android:pathData="M833.7,209.6c-13.2,-13.2 -34.5,-13.2 -47.6,0 -13.2,13.2 -13.2,34.5 0,47.6 122.3,122.3 140,314.4 42.1,456.6 -12.5,18.1 -26.5,35 -42.1,50.5 -6.6,6.6 -9.9,15.2 -9.9,23.8 0,8.6 3.3,17.2 9.9,23.8 13.2,13.2 34.5,13.2 47.6,0 18.4,-18.4 35.1,-38.5 49.9,-60C1000,583.1 979,355 833.7,209.6zM515.1,166.8c-48,-22.3 -102.9,-15.1 -143.4,19L176.6,349.9h-50.2c-31,0 -63.3,25.2 -63.3,56.3v209.4c0,31 32.2,56.3 63.3,56.3h50.2L371.7,836c24.9,21 55.4,31.8 86.2,31.8 19.3,0 38.7,-4.2 57.2,-12.8 48,-22.3 77.8,-69.1 77.8,-122L592.9,288.8c0,-52.9 -29.8,-99.6 -77.8,-122zM531.1,732.9c0,31.8 -17.2,52.9 -46.1,66.3 -28.9,13.4 -56.7,6.2 -81,-14.3L206.2,623.5c-4.9,-4.2 -11.2,-6.4 -17.6,-6.4h-60.2c-0.8,0 -1.5,-0.7 -1.5,-1.5L126.9,406.2c0,-0.8 0.7,-1.5 1.5,-1.5h60.2c6.5,0 12.7,-2.3 17.6,-6.4L412,237.7c24.4,-20.5 51.2,-24.7 80,-11.3 28.9,13.4 39.1,30.5 39.1,62.3v444.2z" />
</vector>

View File

@@ -1,8 +1,8 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="1024"
android:viewportWidth="1024">
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M0,512C0,229.23 229.81,0 512,0 794.77,0 1024,229.81 1024,512 1024,794.77 794.19,1024 512,1024 229.23,1024 0,794.19 0,512Z" />

View File

@@ -1,8 +1,8 @@
<vector android:height="24dp"
android:viewportHeight="1024"
android:viewportWidth="1024"
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
android:height="24dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<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" />

View File

@@ -1,10 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M480,520Q430,520 395,485Q360,450 360,400Q360,350 395,315Q430,280 480,280Q530,280 565,315Q600,350 600,400Q600,450 565,485Q530,520 480,520ZM240,920L240,611Q202,569 181,515Q160,461 160,400Q160,266 253,173Q346,80 480,80Q614,80 707,173Q800,266 800,400Q800,461 779,515Q758,569 720,611L720,920L480,840L240,920ZM480,640Q580,640 650,570Q720,500 720,400Q720,300 650,230Q580,160 480,160Q380,160 310,230Q240,300 240,400Q240,500 310,570Q380,640 480,640ZM320,801L480,760L640,801L640,677Q605,697 564.5,708.5Q524,720 480,720Q436,720 395.5,708.5Q355,697 320,677L320,801ZM480,739L480,739Q480,739 480,739Q480,739 480,739Q480,739 480,739Q480,739 480,739L480,739L480,739Z"/>
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M480,520Q430,520 395,485Q360,450 360,400Q360,350 395,315Q430,280 480,280Q530,280 565,315Q600,350 600,400Q600,450 565,485Q530,520 480,520ZM240,920L240,611Q202,569 181,515Q160,461 160,400Q160,266 253,173Q346,80 480,80Q614,80 707,173Q800,266 800,400Q800,461 779,515Q758,569 720,611L720,920L480,840L240,920ZM480,640Q580,640 650,570Q720,500 720,400Q720,300 650,230Q580,160 480,160Q380,160 310,230Q240,300 240,400Q240,500 310,570Q380,640 480,640ZM320,801L480,760L640,801L640,677Q605,697 564.5,708.5Q524,720 480,720Q436,720 395.5,708.5Q355,697 320,677L320,801ZM480,739L480,739Q480,739 480,739Q480,739 480,739Q480,739 480,739Q480,739 480,739L480,739L480,739Z" />
</vector>

View File

@@ -14,9 +14,9 @@
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:scrollbars="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

View File

@@ -55,11 +55,11 @@
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:scrollbars="vertical"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:nextFocusRight="@+id/fab" />
android:nextFocusRight="@+id/fab"
android:scrollbars="vertical" />
<LinearLayout
android:id="@+id/layout_test"
@@ -68,8 +68,8 @@
android:clickable="true"
android:focusable="true"
android:nextFocusLeft="@+id/recycler_view"
android:orientation="vertical"
android:nextFocusRight="@+id/fab">
android:nextFocusRight="@+id/fab"
android:orientation="vertical">
<View
android:layout_width="wrap_content"
@@ -80,9 +80,9 @@
android:id="@+id/tv_test_state"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="start|center"
android:maxLines="2"
android:minLines="1"
android:gravity="start|center"
android:paddingStart="@dimen/padding_spacing_dp16"
android:text="@string/connection_test_pending"
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
@@ -94,19 +94,18 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="@dimen/padding_spacing_dp16"
android:layout_marginBottom="@dimen/padding_spacing_dp8">
android:layout_marginEnd="@dimen/padding_spacing_dp16">
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginBottom="@dimen/view_height_dp64"
android:layout_marginBottom="@dimen/view_height_dp36"
android:clickable="true"
android:focusable="true"
android:nextFocusLeft="@+id/layout_test"
android:src="@drawable/ic_stat_name"
android:src="@drawable/ic_play_24dp"
app:layout_anchorGravity="bottom|right|end" />
</FrameLayout>

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView 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"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:fitsSystemWindows="true"
tools:context=".ui.SubSettingActivity">
@@ -172,11 +172,11 @@
android:text="@string/routing_settings_outbound_tag" />
<Spinner
android:layout_marginTop="@dimen/padding_spacing_dp8"
android:layout_marginBottom="@dimen/padding_spacing_dp8"
android:id="@+id/sp_outbound_tag"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_spacing_dp8"
android:layout_marginBottom="@dimen/padding_spacing_dp8"
android:entries="@array/outbound_tag" />
</LinearLayout>

View File

@@ -29,13 +29,13 @@
android:text="@string/routing_settings_domain_strategy" />
<Spinner
android:layout_marginTop="@dimen/padding_spacing_dp8"
android:layout_marginBottom="@dimen/padding_spacing_dp8"
android:id="@+id/sp_domain_strategy"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/padding_spacing_dp8"
android:entries="@array/routing_domain_strategy" />
android:layout_marginTop="@dimen/padding_spacing_dp8"
android:layout_marginBottom="@dimen/padding_spacing_dp8"
android:entries="@array/routing_domain_strategy"
android:paddingTop="@dimen/padding_spacing_dp8" />
</LinearLayout>
<LinearLayout

View File

@@ -46,11 +46,11 @@
android:text="@string/server_lab_security3" />
<Spinner
android:layout_marginTop="@dimen/padding_spacing_dp8"
android:layout_marginBottom="@dimen/padding_spacing_dp8"
android:id="@+id/sp_security"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_spacing_dp8"
android:layout_marginBottom="@dimen/padding_spacing_dp8"
android:entries="@array/ss_securitys" />
</LinearLayout>

View File

@@ -45,11 +45,11 @@
android:text="@string/server_lab_flow" />
<Spinner
android:layout_marginTop="@dimen/padding_spacing_dp8"
android:layout_marginBottom="@dimen/padding_spacing_dp8"
android:id="@+id/sp_flow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_spacing_dp8"
android:layout_marginBottom="@dimen/padding_spacing_dp8"
android:entries="@array/flows" />
</LinearLayout>

View File

@@ -45,11 +45,11 @@
android:text="@string/server_lab_security" />
<Spinner
android:layout_marginTop="@dimen/padding_spacing_dp8"
android:layout_marginBottom="@dimen/padding_spacing_dp8"
android:id="@+id/sp_security"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_spacing_dp8"
android:layout_marginBottom="@dimen/padding_spacing_dp8"
android:entries="@array/securitys" />
</LinearLayout>

View File

@@ -52,6 +52,7 @@
android:inputType="text" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView 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"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:fitsSystemWindows="true"
tools:context=".ui.SubSettingActivity">

View File

@@ -16,11 +16,11 @@
android:orientation="vertical">
<Spinner
android:layout_marginTop="@dimen/padding_spacing_dp8"
android:layout_marginBottom="@dimen/padding_spacing_dp8"
android:id="@+id/sp_subscriptionId"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_spacing_dp8"
android:layout_marginBottom="@dimen/padding_spacing_dp8" />
</LinearLayout>

View File

@@ -5,8 +5,8 @@
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:padding="@dimen/padding_spacing_dp8"
android:gravity="center_vertical">
android:gravity="center_vertical"
android:padding="@dimen/padding_spacing_dp8">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/icon"
@@ -25,8 +25,8 @@
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
android:maxLines="1" />
android:maxLines="1"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/package_name"
@@ -43,6 +43,6 @@
android:layout_height="wrap_content"
android:clickable="false"
android:focusable="false"
android:padding="@dimen/padding_spacing_dp8" />
android:padding="@dimen/padding_spacing_dp8" />
</LinearLayout>

View File

@@ -11,9 +11,9 @@
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:visibility="invisible"
android:orientation="horizontal"
android:padding="@dimen/padding_spacing_dp16">
android:padding="@dimen/padding_spacing_dp16"
android:visibility="invisible">
<LinearLayout
android:layout_width="match_parent"

View File

@@ -11,8 +11,8 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/padding_spacing_dp8"
android:orientation="vertical">
android:orientation="vertical"
android:padding="@dimen/padding_spacing_dp8">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/log_tag"
@@ -23,8 +23,8 @@
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/log_content"
android:layout_width="match_parent"
android:paddingTop="@dimen/padding_spacing_dp8"
android:layout_height="wrap_content"
android:paddingTop="@dimen/padding_spacing_dp8"
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
</LinearLayout>

View File

@@ -18,9 +18,9 @@
android:gravity="center"
android:nextFocusRight="@+id/layout_share"
android:orientation="horizontal"
android:paddingStart="@dimen/padding_spacing_dp4"
android:paddingTop="@dimen/padding_spacing_dp8"
android:paddingEnd="@dimen/padding_spacing_dp4"
android:paddingStart="@dimen/padding_spacing_dp4"
android:paddingBottom="@dimen/padding_spacing_dp8">
<LinearLayout

View File

@@ -19,11 +19,11 @@
android:text="@string/server_lab_stream_security" />
<Spinner
android:layout_marginTop="@dimen/padding_spacing_dp8"
android:layout_marginBottom="@dimen/padding_spacing_dp8"
android:id="@+id/sp_stream_security"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_spacing_dp8"
android:layout_marginBottom="@dimen/padding_spacing_dp8"
android:entries="@array/streamsecurityxs"
android:nextFocusDown="@+id/et_sni" />
@@ -54,8 +54,8 @@
android:id="@+id/lay_stream_fingerprint"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="@dimen/padding_spacing_dp16">
android:layout_marginBottom="@dimen/padding_spacing_dp16"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
@@ -63,11 +63,11 @@
android:text="@string/server_lab_stream_fingerprint" />
<Spinner
android:layout_marginTop="@dimen/padding_spacing_dp8"
android:layout_marginBottom="@dimen/padding_spacing_dp8"
android:id="@+id/sp_stream_fingerprint"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_spacing_dp8"
android:layout_marginBottom="@dimen/padding_spacing_dp8"
android:entries="@array/streamsecurity_utls"
android:nextFocusDown="@+id/sp_stream_alpn" />
@@ -77,8 +77,8 @@
android:id="@+id/lay_stream_alpn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="@dimen/padding_spacing_dp16">
android:layout_marginBottom="@dimen/padding_spacing_dp16"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
@@ -86,11 +86,11 @@
android:text="@string/server_lab_stream_alpn" />
<Spinner
android:layout_marginTop="@dimen/padding_spacing_dp8"
android:layout_marginBottom="@dimen/padding_spacing_dp8"
android:id="@+id/sp_stream_alpn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_spacing_dp8"
android:layout_marginBottom="@dimen/padding_spacing_dp8"
android:entries="@array/streamsecurity_alpn"
android:nextFocusDown="@+id/sp_allow_insecure" />
@@ -108,11 +108,11 @@
android:text="@string/server_lab_allow_insecure" />
<Spinner
android:layout_marginTop="@dimen/padding_spacing_dp8"
android:layout_marginBottom="@dimen/padding_spacing_dp8"
android:id="@+id/sp_allow_insecure"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_spacing_dp8"
android:layout_marginBottom="@dimen/padding_spacing_dp8"
android:entries="@array/allowinsecures" />
</LinearLayout>

View File

@@ -19,11 +19,11 @@
android:text="@string/server_lab_stream_security" />
<Spinner
android:layout_marginTop="@dimen/padding_spacing_dp8"
android:layout_marginBottom="@dimen/padding_spacing_dp8"
android:id="@+id/sp_stream_security"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_spacing_dp8"
android:layout_marginBottom="@dimen/padding_spacing_dp8"
android:entries="@array/streamsecuritys"
android:nextFocusDown="@+id/et_sni" />
@@ -63,11 +63,11 @@
android:text="@string/server_lab_allow_insecure" />
<Spinner
android:layout_marginTop="@dimen/padding_spacing_dp8"
android:layout_marginBottom="@dimen/padding_spacing_dp8"
android:id="@+id/sp_allow_insecure"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_spacing_dp8"
android:layout_marginBottom="@dimen/padding_spacing_dp8"
android:entries="@array/allowinsecures" />

View File

@@ -29,11 +29,11 @@
android:text="@string/server_lab_network" />
<Spinner
android:layout_marginTop="@dimen/padding_spacing_dp8"
android:layout_marginBottom="@dimen/padding_spacing_dp8"
android:id="@+id/sp_network"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_spacing_dp8"
android:layout_marginBottom="@dimen/padding_spacing_dp8"
android:entries="@array/networks" />
</LinearLayout>
@@ -50,11 +50,11 @@
android:text="@string/server_lab_head_type" />
<Spinner
android:layout_marginTop="@dimen/padding_spacing_dp8"
android:layout_marginBottom="@dimen/padding_spacing_dp8"
android:id="@+id/sp_header_type"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_spacing_dp8"
android:layout_marginBottom="@dimen/padding_spacing_dp8" />
</LinearLayout>
<LinearLayout

View File

@@ -26,6 +26,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_name"
android:textColor="@android:color/white"
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textColor="@android:color/white" />
</LinearLayout>

View File

@@ -16,6 +16,10 @@
android:id="@+id/routing_setting"
android:icon="@drawable/ic_routing_24dp"
android:title="@string/routing_settings_title" />
<item
android:id="@+id/user_asset_setting"
android:icon="@drawable/ic_file_24dp"
android:title="@string/title_user_asset_setting" />
<item
android:id="@+id/settings"
android:icon="@drawable/ic_settings_24dp"

View File

@@ -57,28 +57,28 @@
android:title="@string/menu_item_import_config_manually_hysteria2"
app:showAsAction="never" />
<!-- <item-->
<!-- android:title="@string/menu_item_import_config_custom"-->
<!-- app:showAsAction="ifRoom">-->
<!-- <menu>-->
<!-- <item-->
<!-- android:id="@+id/import_config_custom_clipboard"-->
<!-- android:title="@string/menu_item_import_config_custom_clipboard"-->
<!-- app:showAsAction="never" />-->
<!-- <item-->
<!-- android:id="@+id/import_config_custom_local"-->
<!-- android:title="@string/menu_item_import_config_custom_local"-->
<!-- app:showAsAction="never" />-->
<!-- <item-->
<!-- android:id="@+id/import_config_custom_url"-->
<!-- android:title="@string/menu_item_import_config_custom_url"-->
<!-- app:showAsAction="never" />-->
<!-- <item-->
<!-- android:id="@+id/import_config_custom_url_scan"-->
<!-- android:title="@string/menu_item_import_config_custom_url_scan"-->
<!-- app:showAsAction="never" />-->
<!-- </menu>-->
<!-- </item>-->
<!-- <item-->
<!-- android:title="@string/menu_item_import_config_custom"-->
<!-- app:showAsAction="ifRoom">-->
<!-- <menu>-->
<!-- <item-->
<!-- android:id="@+id/import_config_custom_clipboard"-->
<!-- android:title="@string/menu_item_import_config_custom_clipboard"-->
<!-- app:showAsAction="never" />-->
<!-- <item-->
<!-- android:id="@+id/import_config_custom_local"-->
<!-- android:title="@string/menu_item_import_config_custom_local"-->
<!-- app:showAsAction="never" />-->
<!-- <item-->
<!-- android:id="@+id/import_config_custom_url"-->
<!-- android:title="@string/menu_item_import_config_custom_url"-->
<!-- app:showAsAction="never" />-->
<!-- <item-->
<!-- android:id="@+id/import_config_custom_url_scan"-->
<!-- android:title="@string/menu_item_import_config_custom_url_scan"-->
<!-- app:showAsAction="never" />-->
<!-- </menu>-->
<!-- </item>-->
</menu>
</item>
<item

View File

@@ -6,10 +6,6 @@
android:icon="@drawable/ic_add_24dp"
android:title="@string/routing_settings_add_rule"
app:showAsAction="ifRoom" />
<item
android:id="@+id/user_asset_setting"
android:icon="@drawable/ic_file_24dp"
android:title="@string/title_user_asset_setting" />
<item
android:id="@+id/import_predefined_rulesets"
android:title="@string/routing_settings_import_predefined_rulesets"

View File

@@ -268,7 +268,7 @@
<string name="title_sub_update">تحديث الاشتراك (أول خطوة)</string>
<string name="title_ping_all_server">Tcping لجميع الإعدادات</string>
<string name="title_real_ping_all_server"> اختبر جميع الإعدادات (3)</string>
<string name="title_user_asset_setting">ملفات أصول جغرافية</string>
<string name="title_user_asset_setting">Asset files</string>
<string name="title_sort_by_test_results">الفرز حسب نتائج الاختبار (5)</string>
<string name="title_filter_config">تصفية ملف التكوين</string>
<string name="filter_config_all">جميع مجموعات الاشتراك</string>

View File

@@ -268,7 +268,7 @@
<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_user_asset_setting">Asset files</string>
<string name="title_sort_by_test_results">টেস্ট ফলাফল দ্বারা সাজানো</string>
<string name="title_filter_config">কনফিগারেশন ফাইল ফিল্টার করুন</string>
<string name="filter_config_all">সব সাবস্ক্রিপশন গ্রুপ</string>

View File

@@ -268,7 +268,7 @@
<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_user_asset_setting">Asset files</string>
<string name="title_sort_by_test_results">ترتیب و ری نتیجیل آزمایش</string>
<string name="title_filter_config">فیلتر کردن کانفیگا</string>
<string name="filter_config_all">پوی جرگیل</string>

View File

@@ -214,8 +214,8 @@
<string name="title_pref_append_http_proxy">پروکسی HTTP را به VPN اضافه کنید</string>
<string name="summary_pref_append_http_proxy">پروکسی HTTP مستقیماً از (مرورگر/برخی برنامه‌های پشتیبانی‌شده)، بدون استفاده از دستگاه NIC مجازی (Android 10+) استفاده می‌شود.</string>
<string name="title_pref_double_column_display">Enable double column display</string>
<string name="summary_pref_double_column_display">The profile list is displayed in double columns, allowing more content to be displayed on the screen. You need to restart the application to take effect.</string>
<string name="title_pref_double_column_display">فعال کردن نمایش دو ستون</string>
<string name="summary_pref_double_column_display">لیست نمایه در دو ستون نمایش داده می شود و امکان نمایش محتوای بیشتری را بر روی صفحه نمایش می دهد. برای اجرا باید برنامه را مجددا راه اندازی کنید.</string>
<!-- AboutActivity -->
<string name="title_pref_feedback">بازخورد</string>
@@ -242,7 +242,7 @@
<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_ui_settings">تنظیمات رابط کاربری</string>
<string name="title_pref_ui_mode_night">تنظیمات حالت رابط کاربری</string>
<string name="title_logcat">گزارشات</string>
@@ -265,7 +265,7 @@
<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_user_asset_setting">فایل های منبع جغرافیایی</string>
<string name="title_sort_by_test_results">مرتب‌ سازی بر اساس نتایج آزمایش</string>
<string name="title_filter_config">فیلتر کردن کانفیگ‌ها</string>
<string name="filter_config_all">همه گروه‌های اشتراک</string>
@@ -327,10 +327,10 @@
<string-array name="share_method_more">
<item>QRcode</item>
<item>Export to clipboard</item>
<item>Export full configuration to clipboard</item>
<item>Edit</item>
<item>Delete</item>
<item>صادر کردن به کلیپ بورد</item>
<item>صادر کردن پیکربندی کامل به کلیپ بورد</item>
<item>ویرایش کردن</item>
<item>حذف کردن</item>
</string-array>
<string-array name="share_sub_method">

View File

@@ -215,8 +215,8 @@
<string name="title_pref_append_http_proxy">Дополнительный HTTP-прокси</string>
<string name="summary_pref_append_http_proxy">HTTP-прокси будет использоваться напрямую (из браузера и других поддерживающих приложений), минуя виртуальный сетевой адаптер (Android 10+)</string>
<string name="title_pref_double_column_display">Enable double column display</string>
<string name="summary_pref_double_column_display">The profile list is displayed in double columns, allowing more content to be displayed on the screen. You need to restart the application to take effect.</string>
<string name="title_pref_double_column_display">Отображение в два столбца</string>
<string name="summary_pref_double_column_display">Список профилей выводится в виде двух столбцов, что позволяет показать больше информации на экране. Требуется перезапуск приложения.</string>
<!-- AboutActivity -->
<string name="title_pref_feedback">Обратная связь</string>
@@ -267,7 +267,7 @@
<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_user_asset_setting">Файлы ресурсов</string>
<string name="title_sort_by_test_results">Сортировка по результатам теста</string>
<string name="title_filter_config">Фильтр групп</string>
<string name="filter_config_all">Все группы</string>
@@ -328,11 +328,11 @@
</string-array>
<string-array name="share_method_more">
<item>QRcode</item>
<item>Export to clipboard</item>
<item>Export full configuration to clipboard</item>
<item>Edit</item>
<item>Delete</item>
<item>QR-код</item>
<item>Экспорт в буфер обмена</item>
<item>Экспорт всей конфигурации в буфер обмена</item>
<item>Изменить</item>
<item>Удалить</item>
</string-array>
<string-array name="share_sub_method">

View File

@@ -268,7 +268,7 @@
<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">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_user_asset_setting">Asset files</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>

Some files were not shown because too many files have changed in this diff Show More