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 popd
- name: Download libv2ray - name: Download libv2ray
uses: robinraju/release-downloader@v1.12 uses: robinraju/release-downloader@v1.13
with: with:
repository: '2dust/AndroidLibXrayLite' repository: '2dust/AndroidLibXrayLite'
tag: ${{ env.CURRENT_TAG }} tag: ${{ env.CURRENT_TAG }}
+2 -2
View File
@@ -12,8 +12,8 @@ android {
applicationId = "com.v2ray.ang" applicationId = "com.v2ray.ang"
minSdk = 24 minSdk = 24
targetSdk = 36 targetSdk = 36
versionCode = 721 versionCode = 722
versionName = "2.1.1" versionName = "2.1.2"
multiDexEnabled = true multiDexEnabled = true
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';') val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
@@ -219,6 +219,8 @@ object AppConfig {
const val REALITY = "reality" const val REALITY = "reality"
const val HEADER_TYPE_HTTP = "http" 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_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_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") 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) LogUtil.i(AppConfig.TAG, url)
val userAgent = it.subscription.userAgent val userAgent = it.subscription.userAgent
val proxyUsername = SettingsManager.getSocksUsername()
val proxyPassword = SettingsManager.getSocksPassword()
var configText = try { var configText = try {
val httpPort = SettingsManager.getHttpPort() val httpPort = SettingsManager.getHttpPort()
HttpUtil.getUrlContentWithUserAgent(url, userAgent, 15000, httpPort) HttpUtil.getUrlContentWithUserAgent(url, userAgent, 15000, httpPort, proxyUsername, proxyPassword)
} catch (e: Exception) { } catch (e: Exception) {
LogUtil.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error", e) LogUtil.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error", e)
"" ""
@@ -317,11 +317,7 @@ object SettingsManager {
} }
private fun generateRandomSocksPort(): Int { private fun generateRandomSocksPort(): Int {
return if (Utils.isXray()) { return Random.nextInt(10000, 65535)
Random.nextInt(10000, 65536)
} else {
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? { fun getRemoteIPInfo(): String? {
val url = MmkvManager.decodeSettingsString(AppConfig.PREF_IP_API_URL) val url = MmkvManager.decodeSettingsString(AppConfig.PREF_IP_API_URL)
.takeIf { !it.isNullOrBlank() } ?: AppConfig.IP_API_URL .takeIf { !it.isNullOrBlank() } ?: AppConfig.IP_API_URL
val proxyUsername = SettingsManager.getSocksUsername()
val proxyPassword = SettingsManager.getSocksPassword()
val httpPort = SettingsManager.getHttpPort() 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 ipInfo = JsonUtil.fromJson(content, IPAPIInfo::class.java) ?: return null
val ip = listOf( val ip = listOf(
@@ -23,10 +23,13 @@ object UpdateCheckerManager {
AppConfig.APP_API_URL.concatUrl("latest") AppConfig.APP_API_URL.concatUrl("latest")
} }
val proxyUsername = SettingsManager.getSocksUsername()
val proxyPassword = SettingsManager.getSocksPassword()
var response = HttpUtil.getUrlContent(url, 5000) var response = HttpUtil.getUrlContent(url, 5000)
if (response.isNullOrEmpty()) { if (response.isNullOrEmpty()) {
val httpPort = SettingsManager.getHttpPort() val httpPort = SettingsManager.getHttpPort()
response = HttpUtil.getUrlContent(url, 5000, httpPort) response = HttpUtil.getUrlContent(url, 5000, httpPort, proxyUsername, proxyPassword)
?: throw IllegalStateException("Failed to get response") ?: 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 { private fun compareVersions(version1: String, version2: String): Int {
val v1 = version1.split(".") val v1 = version1.split(".")
val v2 = version2.split(".") val v2 = version2.split(".")
@@ -98,36 +98,46 @@ object V2rayConfigManager {
return result return result
} }
// check if tun inbound exists val json = JsonUtil.parseString(raw)?.takeIf { it.isJsonObject }?.asJsonObject ?: return result
val json = JsonUtil.parseString(raw) ?: return result
val inboundsJson = if (json.has("inbounds") && json.get("inbounds")?.isJsonNull == false) {
json.getAsJsonArray("inbounds")
} else {
JsonArray()
}
for (i in 0 until inboundsJson.size()) { // Check whether package names need to be replaced with UIDs
val elem = inboundsJson.get(i) if (SettingsManager.canUseProcessRouting()) {
if (elem.isJsonObject) { val rulesJson = json.get("routing")?.takeIf { it.isJsonObject }?.asJsonObject
val inb = elem.asJsonObject ?.get("rules")?.takeIf { it.isJsonArray }?.asJsonArray
val tag = if (inb.has("tag") && inb.get("tag")?.isJsonNull == false) inb.get("tag").asString else "" ?: JsonArray()
if (tag == "tun") return result
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 // check if tun inbound exists
val templateConfig = initV2rayConfig(context) ?: return result val inboundsJson = json.get("inbounds")?.takeIf { it.isJsonArray }?.asJsonArray
val inboundTun = templateConfig.inbounds.firstOrNull { it.tag == "tun" } ?: return result ?: JsonArray().also { json.add("inbounds", it) }
inboundTun.settings?.mtu = SettingsManager.getVpnMtu() val tunNotExists = inboundsJson.none { elem ->
elem.isJsonObject && elem.asJsonObject.get("protocol")
// add to json ?.takeIf { it.isJsonPrimitive && it.asJsonPrimitive.isString }
inboundsJson.add(JsonUtil.parseString(JsonUtil.toJson(inboundTun))) ?.asString == "tun"
if (inboundsJson.size() == 1) {
json.add("inbounds", inboundsJson)
} }
val updatedRaw = JsonUtil.toJsonPretty(json) ?: return result if (tunNotExists) {
return ConfigResult(true, guid, updatedRaw) // 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.tag = EConfigType.HTTP.name.lowercase()
inbound2.port = SettingsManager.getHttpPort() inbound2.port = SettingsManager.getHttpPort()
inbound2.protocol = EConfigType.HTTP.name.lowercase() inbound2.protocol = EConfigType.HTTP.name.lowercase()
inbound2.settings?.auth = null
inbound2.settings?.udp = null
v2rayConfig.inbounds.add(inbound2) v2rayConfig.inbounds.add(inbound2)
} }
@@ -1,5 +1,6 @@
package com.v2ray.ang.ui package com.v2ray.ang.ui
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
@@ -7,11 +8,13 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityAppPickerBinding import com.v2ray.ang.databinding.ActivityAppPickerBinding
import com.v2ray.ang.dto.AppInfo import com.v2ray.ang.dto.AppInfo
import com.v2ray.ang.util.AppManagerUtil import com.v2ray.ang.util.AppManagerUtil
import com.v2ray.ang.util.LogUtil import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.PackageUidResolver
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -101,6 +104,21 @@ class AppPickerActivity : BaseActivity() {
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider) 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() { private fun loadApps() {
showLoading() showLoading()
@@ -108,7 +126,8 @@ class AppPickerActivity : BaseActivity() {
try { try {
val apps = withContext(Dispatchers.IO) { val apps = withContext(Dispatchers.IO) {
val appsList = AppManagerUtil.loadNetworkAppList(this@AppPickerActivity) val appsList = AppManagerUtil.loadNetworkAppList(this@AppPickerActivity)
sortApps(appsList) val sortedApps = sortApps(appsList)
listOf(createSpecialItemUnidentified()) + sortedApps
} }
appsAll = apps appsAll = apps
@@ -191,8 +191,10 @@ class PerAppProxyActivity : BaseActivity() {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
var content = HttpUtil.getUrlContent(url, 5000) var content = HttpUtil.getUrlContent(url, 5000)
if (content.isNullOrEmpty()) { if (content.isNullOrEmpty()) {
val proxyUsername = SettingsManager.getSocksUsername()
val proxyPassword = SettingsManager.getSocksPassword()
val httpPort = SettingsManager.getHttpPort() val httpPort = SettingsManager.getHttpPort()
content = HttpUtil.getUrlContent(url, 5000, httpPort) ?: "" content = HttpUtil.getUrlContent(url, 5000, httpPort, proxyUsername, proxyPassword) ?: ""
} }
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
//LogUtil.i(AppConfig.TAG, content) //LogUtil.i(AppConfig.TAG, content)
@@ -173,9 +173,11 @@ class UserAssetActivity : HelperBaseActivity() {
showLoading() showLoading()
toast(R.string.msg_downloading_content) toast(R.string.msg_downloading_content)
val proxyUsername = SettingsManager.getSocksUsername()
val proxyPassword = SettingsManager.getSocksPassword()
val httpPort = SettingsManager.getHttpPort() val httpPort = SettingsManager.getHttpPort()
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
val result = viewModel.downloadGeoFiles(extDir, httpPort) val result = viewModel.downloadGeoFiles(extDir, httpPort, proxyUsername, proxyPassword)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (result.successCount > 0) { if (result.successCount > 0) {
toast(getString(R.string.title_update_config_count, result.successCount)) 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
import com.v2ray.ang.AppConfig.LOOPBACK import com.v2ray.ang.AppConfig.LOOPBACK
import com.v2ray.ang.BuildConfig import com.v2ray.ang.BuildConfig
import com.v2ray.ang.util.Utils.encode import java.io.File
import com.v2ray.ang.util.Utils.urlDecode
import java.io.IOException import java.io.IOException
import java.net.HttpURLConnection
import java.net.IDN import java.net.IDN
import java.net.Inet6Address import java.net.Inet6Address
import java.net.InetAddress import java.net.InetAddress
@@ -15,6 +13,10 @@ import java.net.MalformedURLException
import java.net.Proxy import java.net.Proxy
import java.net.URI import java.net.URI
import java.net.URL import java.net.URL
import java.util.concurrent.TimeUnit
import okhttp3.Credentials
import okhttp3.OkHttpClient
import okhttp3.Request
object HttpUtil { object HttpUtil {
@@ -106,13 +108,31 @@ object HttpUtil {
* @param httpPort The HTTP port to use. * @param httpPort The HTTP port to use.
* @return The content of the URL as a string. * @return The content of the URL as a string.
*/ */
fun getUrlContent(url: String, timeout: Int, httpPort: Int = 0): String? { fun getUrlContent(
val conn = createProxyConnection(url, httpPort, timeout, timeout) ?: return null 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 { try {
return conn.inputStream.bufferedReader().readText() client.newCall(requestBuilder.build()).execute().use { response ->
} catch (_: Exception) { if (!response.isSuccessful) {
} finally { LogUtil.w(AppConfig.TAG, "Failed to get URL content, code=${response.code}")
conn.disconnect() return null
}
return response.body?.string()
}
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to get URL content", e)
} }
return null return null
} }
@@ -127,127 +147,147 @@ object HttpUtil {
* @throws IOException If an I/O error occurs. * @throws IOException If an I/O error occurs.
*/ */
@Throws(IOException::class) @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 currentUrl = url
var redirects = 0 var redirects = 0
val maxRedirects = 3 val maxRedirects = 3
while (redirects++ < maxRedirects) { while (redirects++ < maxRedirects) {
if (currentUrl == null) continue 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()) { val finalUserAgent = if (userAgent.isNullOrBlank()) {
"v2rayNG/${BuildConfig.VERSION_NAME}" "v2rayNG/${BuildConfig.VERSION_NAME}"
} else { } else {
userAgent userAgent
} }
conn.setRequestProperty("User-agent", finalUserAgent) val requestBuilder = Request.Builder()
conn.connect() .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 client.newCall(requestBuilder.build()).execute().use { response ->
when (responseCode) { when {
in 300..399 -> { response.isRedirect -> {
val location = resolveLocation(conn) val location = response.header("Location")
conn.disconnect() if (location.isNullOrEmpty()) {
if (location.isNullOrEmpty()) { throw IOException("Redirect location not found")
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 { response.isSuccessful -> {
return conn.inputStream.use { it.bufferedReader().readText() } return response.body?.string() ?: ""
} finally { }
conn.disconnect()
else -> {
throw IOException("Request failed with status code ${response.code}")
}
} }
} }
} }
throw IOException("Too many redirects") throw IOException("Too many redirects")
} }
/** private fun buildOkHttpClient(
* Creates an HttpURLConnection object connected through a proxy. timeout: Int,
* httpPort: Int,
* @param urlStr The target URL address. proxyUsername: String?,
* @param port The port of the proxy server. proxyPassword: String?,
* @param connectTimeout The connection timeout in milliseconds (default is 15000 ms). followRedirects: Boolean
* @param readTimeout The read timeout in milliseconds (default is 15000 ms). ): OkHttpClient {
* @param needStream Whether the connection needs to support streaming. val builder = OkHttpClient.Builder()
* @return Returns a configured HttpURLConnection object, or null if it fails. .connectTimeout(timeout.toLong(), TimeUnit.MILLISECONDS)
*/ .readTimeout(timeout.toLong(), TimeUnit.MILLISECONDS)
fun createProxyConnection( .followRedirects(followRedirects)
urlStr: String, .followSslRedirects(followRedirects)
port: Int,
connectTimeout: Int = 15000,
readTimeout: Int = 15000,
needStream: Boolean = false
): HttpURLConnection? {
var conn: HttpURLConnection? = null if (httpPort != 0) {
try { builder.proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress(LOOPBACK, httpPort)))
val url = URL(urlStr) if (!proxyUsername.isNullOrBlank() && !proxyPassword.isNullOrBlank()) {
// Create a connection builder.proxyAuthenticator { _, response ->
conn = if (port == 0) { if (response.request.header("Proxy-Authorization") != null) {
url.openConnection() null
} else { } else {
url.openConnection( response.request.newBuilder()
Proxy( .header("Proxy-Authorization", Credentials.basic(proxyUsername, proxyPassword))
Proxy.Type.HTTP, .build()
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
} }
//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 private fun resolveLocation(baseUrl: String, raw: String): String? {
fun resolveLocation(conn: HttpURLConnection): String? {
val raw = conn.getHeaderField("Location")?.trim()?.takeIf { it.isNotEmpty() } ?: return null
// Try check url is relative or absolute
return try { return try {
val locUri = URI(raw) val locUri = URI(raw)
val baseUri = conn.url.toURI() val baseUri = URI(baseUrl)
val resolved = if (locUri.isAbsolute) locUri else baseUri.resolve(locUri) val resolved = if (locUri.isAbsolute) locUri else baseUri.resolve(locUri)
resolved.toURL().toString() resolved.toURL().toString()
} catch (_: Exception) { } catch (_: Exception) {
// Fallback: url resolver, also should handles //host/...
try { try {
URL(raw).toString() // absolute with protocol URL(raw).toString()
} catch (_: MalformedURLException) { } catch (_: MalformedURLException) {
try { try {
URL(conn.url, raw).toString() URL(URL(baseUrl), raw).toString()
} catch (_: MalformedURLException) { } catch (_: MalformedURLException) {
null 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.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import com.v2ray.ang.AppConfig
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
object PackageUidResolver { object PackageUidResolver {
private const val TAG = "PackageUidResolver"
// In-process cache to avoid resolving the same package UID repeatedly. // In-process cache to avoid resolving the same package UID repeatedly.
private val packageUidCache = ConcurrentHashMap<String, String>() private val packageUidCache = ConcurrentHashMap<String, String>()
@@ -32,13 +31,20 @@ object PackageUidResolver {
} }
private fun resolveUid(context: Context, packageName: String): String? { 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 { return try {
val uid = context.packageManager.getPackageUid(packageName, 0).toString() val uid = context.packageManager.getPackageUid(packageName, 0).toString()
LogUtil.d(TAG, "Package: $packageName -> UID: $uid") LogUtil.d(AppConfig.TAG, "Package: $packageName -> UID: $uid")
uid uid
} catch (_: PackageManager.NameNotFoundException) { } catch (_: PackageManager.NameNotFoundException) {
LogUtil.w(TAG, "Package not found: $packageName") LogUtil.w(AppConfig.TAG, "Package not found: $packageName")
null null
} }
} }
} }
@@ -10,8 +10,6 @@ import com.v2ray.ang.util.HttpUtil
import com.v2ray.ang.util.LogUtil import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import java.io.File import java.io.File
import java.io.FileOutputStream
import java.net.HttpURLConnection
class UserAssetViewModel : ViewModel() { class UserAssetViewModel : ViewModel() {
private val assets = mutableListOf<AssetUrlCache>() 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() val snapshot = getAssets()
var successCount = 0 var successCount = 0
val failures = mutableListOf<String>() val failures = mutableListOf<String>()
@@ -69,7 +72,7 @@ class UserAssetViewModel : ViewModel() {
snapshot.forEach { cache -> snapshot.forEach { cache ->
val item = cache.assetUrl val item = cache.assetUrl
val portsToTry = if (httpPort == 0) listOf(0) else listOf(httpPort, 0) 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++ successCount++
} else { } else {
failures.add(item.remarks) failures.add(item.remarks)
@@ -79,25 +82,22 @@ class UserAssetViewModel : ViewModel() {
return GeoDownloadResult(successCount, failures.size, failures) 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 targetTemp = File(extDir, item.remarks + "_temp")
val target = File(extDir, item.remarks) val target = File(extDir, item.remarks)
val conn = HttpUtil.createProxyConnection(item.url, httpPort, 15000, 15000, needStream = true) ?: return false
try { try {
val responseCode = conn.responseCode if (HttpUtil.downloadToFile(item.url, targetTemp, 15000, httpPort, proxyUsername, proxyPassword)) {
if (responseCode == HttpURLConnection.HTTP_OK) {
conn.inputStream.use { inputStream ->
FileOutputStream(targetTemp).use { output ->
inputStream.copyTo(output)
}
}
targetTemp.renameTo(target) targetTemp.renameTo(target)
return true return true
} }
} catch (e: Exception) { } catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to download geo file: ${item.remarks}", e) LogUtil.e(AppConfig.TAG, "Failed to download geo file: ${item.remarks}", e)
} finally {
conn.disconnect()
} }
return false return false
} }
@@ -144,6 +144,7 @@
<string name="menu_item_import_proxy_app">Import from Clipboard</string> <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">Per-app settings</string>
<string name="per_app_proxy_settings_enable">Enable per-app</string> <string name="per_app_proxy_settings_enable">Enable per-app</string>
<string name="app_picker_unknown_app">Unknown app (unidentified UID)</string>
<!-- Preferences --> <!-- Preferences -->
<string name="title_settings">Settings</string> <string name="title_settings">Settings</string>
+1 -1
View File
@@ -1,5 +1,5 @@
[versions] [versions]
agp = "9.1.0" agp = "9.1.1"
desugarJdkLibs = "2.1.5" desugarJdkLibs = "2.1.5"
gradleLicensePlugin = "0.9.8" gradleLicensePlugin = "0.9.8"
kotlin = "2.3.10" kotlin = "2.3.10"