Compare commits

...

8 Commits

Author SHA1 Message Date
2dust 659f2ded09 up 2.1.2 2026-04-24 10:52:42 +08:00
2dust 585d64b0be Bump Android Gradle Plugin to 9.1.1
Update agp from 9.1.0 to 9.1.1 in V2rayNG/gradle/libs.versions.toml to pick up the latest patch fixes and keep the build tooling current.
2026-04-24 10:52:02 +08:00
fanymagnet 2ae2e747e2 Replace package names on UIDs in custom config (#5527)
* Replace package names on UIDs in custom config

* Update V2rayNG/app/src/main/java/com/v2ray/ang/handler/V2rayConfigManager.kt

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fix copilot review & detect tun optimization

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-24 10:39:19 +08:00
2dust 4199cee4e5 Use OkHttp with proxy auth and support SOCKS creds
Replace HttpURLConnection-based networking with OkHttp and add proxy username/password support. Introduces buildOkHttpClient, updated getUrlContent/getUrlContentWithUserAgent signatures, and downloadToFile; callers in AngConfigManager, SpeedtestManager, UpdateCheckerManager, PerAppProxyActivity, UserAssetActivity and UserAssetViewModel now pass SOCKS proxy credentials. Removes createProxyConnection and related URL-resolve logic, clears HTTP inbound auth/udp in V2rayConfigManager, and simplifies SettingsManager port generation. These changes enable authenticated proxy requests, improved redirect handling and more robust file downloads.
2026-04-23 20:40:17 +08:00
dependabot[bot] 8be8a912c4 Bump robinraju/release-downloader from 1.12 to 1.13 (#5520)
Bumps [robinraju/release-downloader](https://github.com/robinraju/release-downloader) from 1.12 to 1.13.
- [Release notes](https://github.com/robinraju/release-downloader/releases)
- [Commits](https://github.com/robinraju/release-downloader/compare/v1.12...v1.13)

---
updated-dependencies:
- dependency-name: robinraju/release-downloader
  dependency-version: '1.13'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-21 19:44:50 +08:00
Kapkap5454 851f22680a Add negative UID handling to process routing (#5509)
* Handle unidentified package UID resolution

Add special handling for unidentified package names.

* Add method to create special unidentified app item

Added a method to create a special unidentified app item for the app picker.

* Support unidentified app UID and label

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-04-19 19:48:59 +08:00
2dust 12abc99bfc Update hev-socks5-tunnel 2026-04-19 19:31:11 +08:00
2dust 07e50637a2 Update AndroidLibXrayLite 2026-04-19 19:30:40 +08:00
18 changed files with 240 additions and 223 deletions
+1 -1
View File
@@ -70,7 +70,7 @@ jobs:
popd
- name: Download libv2ray
uses: robinraju/release-downloader@v1.12
uses: robinraju/release-downloader@v1.13
with:
repository: '2dust/AndroidLibXrayLite'
tag: ${{ env.CURRENT_TAG }}
+2 -2
View File
@@ -12,8 +12,8 @@ android {
applicationId = "com.v2ray.ang"
minSdk = 24
targetSdk = 36
versionCode = 721
versionName = "2.1.1"
versionCode = 722
versionName = "2.1.2"
multiDexEnabled = true
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
@@ -219,6 +219,8 @@ object AppConfig {
const val REALITY = "reality"
const val HEADER_TYPE_HTTP = "http"
const val UNIDENTIFIED_PACKAGE = "__unknown_app__"
val DNS_ALIDNS_ADDRESSES = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1")
val DNS_CLOUDFLARE_ONE_ADDRESSES = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001")
val DNS_CLOUDFLARE_DNS_COM_ADDRESSES = arrayListOf("104.16.132.229", "104.16.133.229", "2606:4700::6810:84e5", "2606:4700::6810:85e5")
@@ -532,10 +532,12 @@ object AngConfigManager {
}
LogUtil.i(AppConfig.TAG, url)
val userAgent = it.subscription.userAgent
val proxyUsername = SettingsManager.getSocksUsername()
val proxyPassword = SettingsManager.getSocksPassword()
var configText = try {
val httpPort = SettingsManager.getHttpPort()
HttpUtil.getUrlContentWithUserAgent(url, userAgent, 15000, httpPort)
HttpUtil.getUrlContentWithUserAgent(url, userAgent, 15000, httpPort, proxyUsername, proxyPassword)
} catch (e: Exception) {
LogUtil.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error", e)
""
@@ -317,11 +317,7 @@ object SettingsManager {
}
private fun generateRandomSocksPort(): Int {
return if (Utils.isXray()) {
Random.nextInt(10000, 65536)
} else {
Random.nextInt(10000, 65535)
}
return Random.nextInt(10000, 65535)
}
/**
@@ -83,49 +83,14 @@ object SpeedtestManager {
}
}
/**
* Tests the connection to a given URL and port.
*
* @param context The Context in which the test is running.
* @param port The port to connect to.
* @return A pair containing the elapsed time in milliseconds and the result message.
*/
fun testConnection(context: Context, port: Int): Pair<Long, String> {
var result: String
var elapsed = -1L
val conn = HttpUtil.createProxyConnection(SettingsManager.getDelayTestUrl(), port, 15000, 15000) ?: return Pair(elapsed, "")
try {
val start = SystemClock.elapsedRealtime()
val code = conn.responseCode
elapsed = SystemClock.elapsedRealtime() - start
result = when (code) {
204 -> context.getString(R.string.connection_test_available, elapsed)
200 if conn.contentLengthLong == 0L -> context.getString(R.string.connection_test_available, elapsed)
else -> throw IOException(
context.getString(R.string.connection_test_error_status_code, code)
)
}
} catch (e: IOException) {
LogUtil.e(AppConfig.TAG, "Connection test IOException", e)
result = context.getString(R.string.connection_test_error, e.message)
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Connection test Exception", e)
result = context.getString(R.string.connection_test_error, e.message)
} finally {
conn.disconnect()
}
return Pair(elapsed, result)
}
fun getRemoteIPInfo(): String? {
val url = MmkvManager.decodeSettingsString(AppConfig.PREF_IP_API_URL)
.takeIf { !it.isNullOrBlank() } ?: AppConfig.IP_API_URL
val proxyUsername = SettingsManager.getSocksUsername()
val proxyPassword = SettingsManager.getSocksPassword()
val httpPort = SettingsManager.getHttpPort()
val content = HttpUtil.getUrlContent(url, 5000, httpPort) ?: return null
val content = HttpUtil.getUrlContent(url, 5000, httpPort, proxyUsername, proxyPassword) ?: return null
val ipInfo = JsonUtil.fromJson(content, IPAPIInfo::class.java) ?: return null
val ip = listOf(
@@ -23,10 +23,13 @@ object UpdateCheckerManager {
AppConfig.APP_API_URL.concatUrl("latest")
}
val proxyUsername = SettingsManager.getSocksUsername()
val proxyPassword = SettingsManager.getSocksPassword()
var response = HttpUtil.getUrlContent(url, 5000)
if (response.isNullOrEmpty()) {
val httpPort = SettingsManager.getHttpPort()
response = HttpUtil.getUrlContent(url, 5000, httpPort)
response = HttpUtil.getUrlContent(url, 5000, httpPort, proxyUsername, proxyPassword)
?: throw IllegalStateException("Failed to get response")
}
@@ -61,39 +64,6 @@ object UpdateCheckerManager {
}
}
suspend fun downloadApk(context: Context, downloadUrl: String): File? = withContext(Dispatchers.IO) {
try {
val httpPort = SettingsManager.getHttpPort()
val connection = HttpUtil.createProxyConnection(downloadUrl, httpPort, 10000, 10000, true)
?: throw IllegalStateException("Failed to create connection")
try {
val apkFile = File(context.cacheDir, "update.apk")
LogUtil.i(AppConfig.TAG, "Downloading APK to: ${apkFile.absolutePath}")
FileOutputStream(apkFile).use { outputStream ->
connection.inputStream.use { inputStream ->
inputStream.copyTo(outputStream)
}
}
LogUtil.i(AppConfig.TAG, "APK download completed")
return@withContext apkFile
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to download APK: ${e.message}")
return@withContext null
} finally {
try {
connection.disconnect()
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Error closing connection: ${e.message}")
}
}
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to initiate download: ${e.message}")
return@withContext null
}
}
private fun compareVersions(version1: String, version2: String): Int {
val v1 = version1.split(".")
val v2 = version2.split(".")
@@ -98,36 +98,46 @@ object V2rayConfigManager {
return result
}
// check if tun inbound exists
val json = JsonUtil.parseString(raw) ?: return result
val inboundsJson = if (json.has("inbounds") && json.get("inbounds")?.isJsonNull == false) {
json.getAsJsonArray("inbounds")
} else {
JsonArray()
}
val json = JsonUtil.parseString(raw)?.takeIf { it.isJsonObject }?.asJsonObject ?: return result
for (i in 0 until inboundsJson.size()) {
val elem = inboundsJson.get(i)
if (elem.isJsonObject) {
val inb = elem.asJsonObject
val tag = if (inb.has("tag") && inb.get("tag")?.isJsonNull == false) inb.get("tag").asString else ""
if (tag == "tun") return result
// Check whether package names need to be replaced with UIDs
if (SettingsManager.canUseProcessRouting()) {
val rulesJson = json.get("routing")?.takeIf { it.isJsonObject }?.asJsonObject
?.get("rules")?.takeIf { it.isJsonArray }?.asJsonArray
?: JsonArray()
for (elem in rulesJson) {
val rule = elem.takeIf { it.isJsonObject }?.asJsonObject ?: continue
val process = rule.get("process")?.takeIf { it.isJsonArray }?.asJsonArray ?: continue
val packages = process.mapNotNull {
it.takeIf { it.isJsonPrimitive && it.asJsonPrimitive.isString }?.asString
}.takeIf { it.isNotEmpty() } ?: continue
val uids = PackageUidResolver.packageNamesToUids(context, packages).takeIf { it.isNotEmpty() } ?: continue
rule.add("process", JsonArray().apply { uids.forEach { add(it) } })
}
}
// add tun inbound from template
val templateConfig = initV2rayConfig(context) ?: return result
val inboundTun = templateConfig.inbounds.firstOrNull { it.tag == "tun" } ?: return result
inboundTun.settings?.mtu = SettingsManager.getVpnMtu()
// add to json
inboundsJson.add(JsonUtil.parseString(JsonUtil.toJson(inboundTun)))
if (inboundsJson.size() == 1) {
json.add("inbounds", inboundsJson)
// check if tun inbound exists
val inboundsJson = json.get("inbounds")?.takeIf { it.isJsonArray }?.asJsonArray
?: JsonArray().also { json.add("inbounds", it) }
val tunNotExists = inboundsJson.none { elem ->
elem.isJsonObject && elem.asJsonObject.get("protocol")
?.takeIf { it.isJsonPrimitive && it.asJsonPrimitive.isString }
?.asString == "tun"
}
val updatedRaw = JsonUtil.toJsonPretty(json) ?: return result
return ConfigResult(true, guid, updatedRaw)
if (tunNotExists) {
// add tun inbound from template
initV2rayConfig(context)?.let { templateConfig ->
templateConfig.inbounds.firstOrNull { it.tag == "tun" }?.let { inboundTun ->
inboundTun.settings?.mtu = SettingsManager.getVpnMtu()
inboundsJson.add(JsonUtil.parseString(JsonUtil.toJson(inboundTun)))
}
}
}
return JsonUtil.toJsonPretty(json)?.let { ConfigResult(true, guid, it) } ?: result
}
/**
@@ -416,6 +426,8 @@ object V2rayConfigManager {
inbound2.tag = EConfigType.HTTP.name.lowercase()
inbound2.port = SettingsManager.getHttpPort()
inbound2.protocol = EConfigType.HTTP.name.lowercase()
inbound2.settings?.auth = null
inbound2.settings?.udp = null
v2rayConfig.inbounds.add(inbound2)
}
@@ -1,5 +1,6 @@
package com.v2ray.ang.ui
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Bundle
@@ -7,11 +8,13 @@ import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.widget.SearchView
import androidx.lifecycle.lifecycleScope
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityAppPickerBinding
import com.v2ray.ang.dto.AppInfo
import com.v2ray.ang.util.AppManagerUtil
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.PackageUidResolver
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -101,6 +104,21 @@ class AppPickerActivity : BaseActivity() {
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
}
@SuppressLint("UseCompatLoadingForDrawables")
private fun createSpecialItemUnidentified(): AppInfo {
val icon = requireNotNull(
getDrawable(android.R.drawable.ic_menu_help)
?: getDrawable(android.R.drawable.sym_def_app_icon)
) { "No fallback drawable available" }
return AppInfo(
appName = getString(R.string.app_picker_unknown_app),
packageName = AppConfig.UNIDENTIFIED_PACKAGE,
appIcon = icon,
isSystemApp = false,
isSelected = 0
)
}
private fun loadApps() {
showLoading()
@@ -108,7 +126,8 @@ class AppPickerActivity : BaseActivity() {
try {
val apps = withContext(Dispatchers.IO) {
val appsList = AppManagerUtil.loadNetworkAppList(this@AppPickerActivity)
sortApps(appsList)
val sortedApps = sortApps(appsList)
listOf(createSpecialItemUnidentified()) + sortedApps
}
appsAll = apps
@@ -191,8 +191,10 @@ class PerAppProxyActivity : BaseActivity() {
lifecycleScope.launch(Dispatchers.IO) {
var content = HttpUtil.getUrlContent(url, 5000)
if (content.isNullOrEmpty()) {
val proxyUsername = SettingsManager.getSocksUsername()
val proxyPassword = SettingsManager.getSocksPassword()
val httpPort = SettingsManager.getHttpPort()
content = HttpUtil.getUrlContent(url, 5000, httpPort) ?: ""
content = HttpUtil.getUrlContent(url, 5000, httpPort, proxyUsername, proxyPassword) ?: ""
}
launch(Dispatchers.Main) {
//LogUtil.i(AppConfig.TAG, content)
@@ -173,9 +173,11 @@ class UserAssetActivity : HelperBaseActivity() {
showLoading()
toast(R.string.msg_downloading_content)
val proxyUsername = SettingsManager.getSocksUsername()
val proxyPassword = SettingsManager.getSocksPassword()
val httpPort = SettingsManager.getHttpPort()
lifecycleScope.launch(Dispatchers.IO) {
val result = viewModel.downloadGeoFiles(extDir, httpPort)
val result = viewModel.downloadGeoFiles(extDir, httpPort, proxyUsername, proxyPassword)
withContext(Dispatchers.Main) {
if (result.successCount > 0) {
toast(getString(R.string.title_update_config_count, result.successCount))
@@ -3,10 +3,8 @@ package com.v2ray.ang.util
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.LOOPBACK
import com.v2ray.ang.BuildConfig
import com.v2ray.ang.util.Utils.encode
import com.v2ray.ang.util.Utils.urlDecode
import java.io.File
import java.io.IOException
import java.net.HttpURLConnection
import java.net.IDN
import java.net.Inet6Address
import java.net.InetAddress
@@ -15,6 +13,10 @@ import java.net.MalformedURLException
import java.net.Proxy
import java.net.URI
import java.net.URL
import java.util.concurrent.TimeUnit
import okhttp3.Credentials
import okhttp3.OkHttpClient
import okhttp3.Request
object HttpUtil {
@@ -106,13 +108,31 @@ object HttpUtil {
* @param httpPort The HTTP port to use.
* @return The content of the URL as a string.
*/
fun getUrlContent(url: String, timeout: Int, httpPort: Int = 0): String? {
val conn = createProxyConnection(url, httpPort, timeout, timeout) ?: return null
fun getUrlContent(
url: String,
timeout: Int,
httpPort: Int = 0,
proxyUsername: String? = null,
proxyPassword: String? = null
): String? {
val client = buildOkHttpClient(timeout, httpPort, proxyUsername, proxyPassword, followRedirects = true)
val requestBuilder = Request.Builder()
.url(url)
.get()
.header("Connection", "close")
if (httpPort != 0 && !proxyUsername.isNullOrBlank() && !proxyPassword.isNullOrBlank()) {
requestBuilder.header("Proxy-Authorization", Credentials.basic(proxyUsername, proxyPassword))
}
try {
return conn.inputStream.bufferedReader().readText()
} catch (_: Exception) {
} finally {
conn.disconnect()
client.newCall(requestBuilder.build()).execute().use { response ->
if (!response.isSuccessful) {
LogUtil.w(AppConfig.TAG, "Failed to get URL content, code=${response.code}")
return null
}
return response.body?.string()
}
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to get URL content", e)
}
return null
}
@@ -127,127 +147,147 @@ object HttpUtil {
* @throws IOException If an I/O error occurs.
*/
@Throws(IOException::class)
fun getUrlContentWithUserAgent(url: String?, userAgent: String?, timeout: Int = 15000, httpPort: Int = 0): String {
fun getUrlContentWithUserAgent(
url: String?,
userAgent: String?,
timeout: Int = 15000,
httpPort: Int = 0,
proxyUsername: String? = null,
proxyPassword: String? = null
): String {
var currentUrl = url
var redirects = 0
val maxRedirects = 3
while (redirects++ < maxRedirects) {
if (currentUrl == null) continue
val conn = createProxyConnection(currentUrl, httpPort, timeout, timeout) ?: continue
val client = buildOkHttpClient(timeout, httpPort, proxyUsername, proxyPassword, followRedirects = false)
val finalUserAgent = if (userAgent.isNullOrBlank()) {
"v2rayNG/${BuildConfig.VERSION_NAME}"
} else {
userAgent
}
conn.setRequestProperty("User-agent", finalUserAgent)
conn.connect()
val requestBuilder = Request.Builder()
.url(currentUrl)
.get()
.header("User-agent", finalUserAgent)
.header("Connection", "close")
if (httpPort != 0 && !proxyUsername.isNullOrBlank() && !proxyPassword.isNullOrBlank()) {
requestBuilder.header("Proxy-Authorization", Credentials.basic(proxyUsername, proxyPassword))
}
val responseCode = conn.responseCode
when (responseCode) {
in 300..399 -> {
val location = resolveLocation(conn)
conn.disconnect()
if (location.isNullOrEmpty()) {
throw IOException("Redirect location not found")
client.newCall(requestBuilder.build()).execute().use { response ->
when {
response.isRedirect -> {
val location = response.header("Location")
if (location.isNullOrEmpty()) {
throw IOException("Redirect location not found")
}
currentUrl = resolveLocation(currentUrl, location)
if (currentUrl.isNullOrEmpty()) {
throw IOException("Failed to resolve redirect location")
}
continue
}
currentUrl = location
continue
}
else -> try {
return conn.inputStream.use { it.bufferedReader().readText() }
} finally {
conn.disconnect()
response.isSuccessful -> {
return response.body?.string() ?: ""
}
else -> {
throw IOException("Request failed with status code ${response.code}")
}
}
}
}
throw IOException("Too many redirects")
}
/**
* Creates an HttpURLConnection object connected through a proxy.
*
* @param urlStr The target URL address.
* @param port The port of the proxy server.
* @param connectTimeout The connection timeout in milliseconds (default is 15000 ms).
* @param readTimeout The read timeout in milliseconds (default is 15000 ms).
* @param needStream Whether the connection needs to support streaming.
* @return Returns a configured HttpURLConnection object, or null if it fails.
*/
fun createProxyConnection(
urlStr: String,
port: Int,
connectTimeout: Int = 15000,
readTimeout: Int = 15000,
needStream: Boolean = false
): HttpURLConnection? {
private fun buildOkHttpClient(
timeout: Int,
httpPort: Int,
proxyUsername: String?,
proxyPassword: String?,
followRedirects: Boolean
): OkHttpClient {
val builder = OkHttpClient.Builder()
.connectTimeout(timeout.toLong(), TimeUnit.MILLISECONDS)
.readTimeout(timeout.toLong(), TimeUnit.MILLISECONDS)
.followRedirects(followRedirects)
.followSslRedirects(followRedirects)
var conn: HttpURLConnection? = null
try {
val url = URL(urlStr)
// Create a connection
conn = if (port == 0) {
url.openConnection()
} else {
url.openConnection(
Proxy(
Proxy.Type.HTTP,
InetSocketAddress(LOOPBACK, port)
)
)
} as HttpURLConnection
// Set connection and read timeouts
conn.connectTimeout = connectTimeout
conn.readTimeout = readTimeout
if (!needStream) {
// Set request headers
conn.setRequestProperty("Connection", "close")
// Disable automatic redirects
conn.instanceFollowRedirects = false
// Disable caching
conn.useCaches = false
if (httpPort != 0) {
builder.proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress(LOOPBACK, httpPort)))
if (!proxyUsername.isNullOrBlank() && !proxyPassword.isNullOrBlank()) {
builder.proxyAuthenticator { _, response ->
if (response.request.header("Proxy-Authorization") != null) {
null
} else {
response.request.newBuilder()
.header("Proxy-Authorization", Credentials.basic(proxyUsername, proxyPassword))
.build()
}
}
}
//Add Basic Authorization
url.userInfo?.let {
conn.setRequestProperty(
"Authorization",
"Basic ${encode(urlDecode(it))}"
)
}
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to create proxy connection", e)
// If an exception occurs, close the connection and return null
conn?.disconnect()
return null
}
return conn
return builder.build()
}
// Returns absolute URL string location header sets
fun resolveLocation(conn: HttpURLConnection): String? {
val raw = conn.getHeaderField("Location")?.trim()?.takeIf { it.isNotEmpty() } ?: return null
// Try check url is relative or absolute
private fun resolveLocation(baseUrl: String, raw: String): String? {
return try {
val locUri = URI(raw)
val baseUri = conn.url.toURI()
val baseUri = URI(baseUrl)
val resolved = if (locUri.isAbsolute) locUri else baseUri.resolve(locUri)
resolved.toURL().toString()
} catch (_: Exception) {
// Fallback: url resolver, also should handles //host/...
try {
URL(raw).toString() // absolute with protocol
URL(raw).toString()
} catch (_: MalformedURLException) {
try {
URL(conn.url, raw).toString()
URL(URL(baseUrl), raw).toString()
} catch (_: MalformedURLException) {
null
}
}
}
}
fun downloadToFile(
url: String,
targetFile: File,
timeout: Int = 15000,
httpPort: Int = 0,
proxyUsername: String? = null,
proxyPassword: String? = null
): Boolean {
val client = buildOkHttpClient(timeout, httpPort, proxyUsername, proxyPassword, followRedirects = true)
val requestBuilder = Request.Builder()
.url(url)
.get()
.header("Connection", "close")
if (httpPort != 0 && !proxyUsername.isNullOrBlank() && !proxyPassword.isNullOrBlank()) {
requestBuilder.header("Proxy-Authorization", Credentials.basic(proxyUsername, proxyPassword))
}
return try {
client.newCall(requestBuilder.build()).execute().use { response ->
if (!response.isSuccessful) {
LogUtil.w(AppConfig.TAG, "Failed to download file, code=${response.code}, url=$url")
return false
}
val body = response.body ?: return false
body.byteStream().use { input ->
targetFile.outputStream().use { output ->
input.copyTo(output)
}
}
true
}
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to download file: $url", e)
false
}
}
}
@@ -2,12 +2,11 @@ package com.v2ray.ang.util
import android.content.Context
import android.content.pm.PackageManager
import com.v2ray.ang.AppConfig
import java.util.concurrent.ConcurrentHashMap
object PackageUidResolver {
private const val TAG = "PackageUidResolver"
// In-process cache to avoid resolving the same package UID repeatedly.
private val packageUidCache = ConcurrentHashMap<String, String>()
@@ -32,13 +31,20 @@ object PackageUidResolver {
}
private fun resolveUid(context: Context, packageName: String): String? {
// Special token for connections whose UID cannot be resolved (mapped to -1)
if (packageName == AppConfig.UNIDENTIFIED_PACKAGE) {
val uid = "-1"
LogUtil.d(AppConfig.TAG, "Special package: $packageName -> UID: $uid")
return uid
}
return try {
val uid = context.packageManager.getPackageUid(packageName, 0).toString()
LogUtil.d(TAG, "Package: $packageName -> UID: $uid")
LogUtil.d(AppConfig.TAG, "Package: $packageName -> UID: $uid")
uid
} catch (_: PackageManager.NameNotFoundException) {
LogUtil.w(TAG, "Package not found: $packageName")
LogUtil.w(AppConfig.TAG, "Package not found: $packageName")
null
}
}
}
}
@@ -10,8 +10,6 @@ import com.v2ray.ang.util.HttpUtil
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.Utils
import java.io.File
import java.io.FileOutputStream
import java.net.HttpURLConnection
class UserAssetViewModel : ViewModel() {
private val assets = mutableListOf<AssetUrlCache>()
@@ -61,7 +59,12 @@ class UserAssetViewModel : ViewModel() {
}
}
fun downloadGeoFiles(extDir: File, httpPort: Int): GeoDownloadResult {
fun downloadGeoFiles(
extDir: File,
httpPort: Int,
proxyUsername: String? = null,
proxyPassword: String? = null
): GeoDownloadResult {
val snapshot = getAssets()
var successCount = 0
val failures = mutableListOf<String>()
@@ -69,7 +72,7 @@ class UserAssetViewModel : ViewModel() {
snapshot.forEach { cache ->
val item = cache.assetUrl
val portsToTry = if (httpPort == 0) listOf(0) else listOf(httpPort, 0)
if (portsToTry.any { tryDownload(item, extDir, it) }) {
if (portsToTry.any { tryDownload(item, extDir, it, proxyUsername, proxyPassword) }) {
successCount++
} else {
failures.add(item.remarks)
@@ -79,25 +82,22 @@ class UserAssetViewModel : ViewModel() {
return GeoDownloadResult(successCount, failures.size, failures)
}
private fun tryDownload(item: AssetUrlItem, extDir: File, httpPort: Int): Boolean {
private fun tryDownload(
item: AssetUrlItem,
extDir: File,
httpPort: Int,
proxyUsername: String? = null,
proxyPassword: String? = null
): Boolean {
val targetTemp = File(extDir, item.remarks + "_temp")
val target = File(extDir, item.remarks)
val conn = HttpUtil.createProxyConnection(item.url, httpPort, 15000, 15000, needStream = true) ?: return false
try {
val responseCode = conn.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) {
conn.inputStream.use { inputStream ->
FileOutputStream(targetTemp).use { output ->
inputStream.copyTo(output)
}
}
if (HttpUtil.downloadToFile(item.url, targetTemp, 15000, httpPort, proxyUsername, proxyPassword)) {
targetTemp.renameTo(target)
return true
}
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to download geo file: ${item.remarks}", e)
} finally {
conn.disconnect()
}
return false
}
@@ -144,6 +144,7 @@
<string name="menu_item_import_proxy_app">Import from Clipboard</string>
<string name="per_app_proxy_settings">Per-app settings</string>
<string name="per_app_proxy_settings_enable">Enable per-app</string>
<string name="app_picker_unknown_app">Unknown app (unidentified UID)</string>
<!-- Preferences -->
<string name="title_settings">Settings</string>
+1 -1
View File
@@ -1,5 +1,5 @@
[versions]
agp = "9.1.0"
agp = "9.1.1"
desugarJdkLibs = "2.1.5"
gradleLicensePlugin = "0.9.8"
kotlin = "2.3.10"