From 4d4a4543c51340eb75283a6580bfc73349209c88 Mon Sep 17 00:00:00 2001 From: Hadi Norouzi Date: Thu, 11 May 2023 20:42:15 +0330 Subject: [PATCH] feature: import subscription from v2rayng://install-sub deeplink --- V2rayNG/app/src/main/AndroidManifest.xml | 5 +- .../com/v2ray/ang/ui/UrlSchemeActivity.kt | 70 +++- .../com/v2ray/ang/util/AngConfigManager.kt | 311 +++++++++++++----- .../app/src/main/res/values-fa/strings.xml | 2 + .../app/src/main/res/values-ru/strings.xml | 2 + .../src/main/res/values-zh-rCN/strings.xml | 2 + V2rayNG/app/src/main/res/values/strings.xml | 3 + 7 files changed, 290 insertions(+), 105 deletions(-) diff --git a/V2rayNG/app/src/main/AndroidManifest.xml b/V2rayNG/app/src/main/AndroidManifest.xml index 799fc968..7d118e8d 100644 --- a/V2rayNG/app/src/main/AndroidManifest.xml +++ b/V2rayNG/app/src/main/AndroidManifest.xml @@ -109,8 +109,9 @@ - + + + diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/UrlSchemeActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/UrlSchemeActivity.kt index 7f18a71e..793b3c29 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/UrlSchemeActivity.kt +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/UrlSchemeActivity.kt @@ -3,10 +3,12 @@ package com.v2ray.ang.ui import android.content.Intent import android.net.Uri import android.os.Bundle +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.util.AngConfigManager +import java.net.URLDecoder class UrlSchemeActivity : BaseActivity() { private lateinit var binding: ActivityLogcatBinding @@ -17,34 +19,68 @@ class UrlSchemeActivity : BaseActivity() { val view = binding.root setContentView(view) - var shareUrl: String = "" try { - intent?.apply { - when (action) { - Intent.ACTION_SEND -> { - if ("text/plain" == type) { - intent.getStringExtra(Intent.EXTRA_TEXT)?.let { - shareUrl = it + intent.apply { + if (action == Intent.ACTION_SEND) { + if ("text/plain" == type) { + intent.getStringExtra(Intent.EXTRA_TEXT)?.let { + val uri = Uri.parse(it) + if (uri.scheme?.startsWith(AppConfig.HTTPS_PROTOCOL) == true || uri.scheme?.startsWith( + AppConfig.HTTP_PROTOCOL + ) == true + ) { + val name = uri.getQueryParameter("name") ?: "Subscription" + importSubscription(it, name) + } else { + importConfig(it) } } } - Intent.ACTION_VIEW -> { - val uri: Uri? = intent.data - shareUrl = uri?.getQueryParameter("url")!! + } else if (action == Intent.ACTION_VIEW) { + when (data?.host) { + "install-config" -> { + val uri: Uri? = intent.data + val shareUrl: String = uri?.getQueryParameter("url")!! + toast(shareUrl) + importConfig(shareUrl) + } + + "install-sub" -> { + val uri: Uri? = intent.data + val url = uri?.getQueryParameter("url")!! + val name = uri.getQueryParameter("name") ?: "Subscription" + importSubscription(url, name) + } + + else -> { + toast(R.string.toast_failure) + } } } + } - toast(shareUrl) - val count = AngConfigManager.importBatchConfig(shareUrl, "", false) - if (count > 0) { - toast(R.string.toast_success) - } else { - toast(R.string.toast_failure) - } + + startActivity(Intent(this, MainActivity::class.java)) finish() } catch (e: Exception) { e.printStackTrace() } } + + private fun importSubscription(url: String, name: String) { + val decodedUrl = URLDecoder.decode(url, "UTF-8") + + val check = AngConfigManager.importSubscription(name, decodedUrl) + if (check) toast(R.string.import_subscription_success) else toast(R.string.import_subscription_failure) + } + + private fun importConfig(shareUrl: String) { + val count = AngConfigManager.importBatchConfig(shareUrl, "", false) + if (count > 0) { + toast(R.string.toast_success) + } else { + toast(R.string.toast_failure) + } + } } \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/AngConfigManager.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/AngConfigManager.kt index eb87b934..7366a7c3 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/AngConfigManager.kt +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/AngConfigManager.kt @@ -20,11 +20,27 @@ import com.v2ray.ang.util.MmkvManager.KEY_SELECTED_SERVER import java.net.URI import java.util.* import com.v2ray.ang.extension.idnHost +import com.v2ray.ang.extension.toast object AngConfigManager { - private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) } - private val serverRawStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SERVER_RAW, MMKV.MULTI_PROCESS_MODE) } - private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) } + private val mainStorage by lazy { + MMKV.mmkvWithID( + MmkvManager.ID_MAIN, + MMKV.MULTI_PROCESS_MODE + ) + } + private val serverRawStorage by lazy { + MMKV.mmkvWithID( + MmkvManager.ID_SERVER_RAW, + MMKV.MULTI_PROCESS_MODE + ) + } + private val settingsStorage by lazy { + MMKV.mmkvWithID( + MmkvManager.ID_SETTING, + MMKV.MULTI_PROCESS_MODE + ) + } private val subStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SUB, MMKV.MULTI_PROCESS_MODE) } /** @@ -82,8 +98,14 @@ object AngConfigManager { ).forEach { key -> settingsStorage?.encode(key, sharedPreferences.getBoolean(key, false)) } - settingsStorage?.encode(AppConfig.PREF_SNIFFING_ENABLED, sharedPreferences.getBoolean(AppConfig.PREF_SNIFFING_ENABLED, true)) - settingsStorage?.encode(AppConfig.PREF_PER_APP_PROXY_SET, sharedPreferences.getStringSet(AppConfig.PREF_PER_APP_PROXY_SET, setOf())) + settingsStorage?.encode( + AppConfig.PREF_SNIFFING_ENABLED, + sharedPreferences.getBoolean(AppConfig.PREF_SNIFFING_ENABLED, true) + ) + settingsStorage?.encode( + AppConfig.PREF_PER_APP_PROXY_SET, + sharedPreferences.getStringSet(AppConfig.PREF_PER_APP_PROXY_SET, setOf()) + ) } private fun migrateVmessBean(angConfig: AngConfig, sharedPreferences: SharedPreferences) { @@ -125,7 +147,8 @@ object AngConfigManager { if (TextUtils.isEmpty(vmessBean.security) && TextUtils.isEmpty(vmessBean.id)) { server.users = null } else { - val socksUsersBean = V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean() + val socksUsersBean = + V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean() socksUsersBean.user = vmessBean.security socksUsersBean.pass = vmessBean.id server.users = listOf(socksUsersBean) @@ -135,17 +158,27 @@ object AngConfigManager { } } config.outboundBean?.streamSettings?.let { streamSetting -> - val sni = streamSetting.populateTransportSettings(vmessBean.network, vmessBean.headerType, - vmessBean.requestHost, vmessBean.path, vmessBean.path, vmessBean.requestHost, vmessBean.path, - vmessBean.headerType, vmessBean.path) + val sni = streamSetting.populateTransportSettings( + vmessBean.network, + vmessBean.headerType, + vmessBean.requestHost, + vmessBean.path, + vmessBean.path, + vmessBean.requestHost, + vmessBean.path, + vmessBean.headerType, + vmessBean.path + ) val allowInsecure = if (vmessBean.allowInsecure.isBlank()) { settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false } else { vmessBean.allowInsecure.toBoolean() } var fingerprint = streamSetting.tlsSettings?.fingerprint - streamSetting.populateTlsSettings(vmessBean.streamSecurity, allowInsecure, - vmessBean.sni.ifBlank { sni }, fingerprint, null, null, null, null) + streamSetting.populateTlsSettings( + vmessBean.streamSecurity, allowInsecure, + vmessBean.sni.ifBlank { sni }, fingerprint, null, null, null, null + ) } } val key = MmkvManager.encodeServerConfig(vmessBean.guid, config) @@ -168,14 +201,21 @@ object AngConfigManager { /** * import config form qrcode or... */ - private fun importConfig(str: String?, subid: String, removedSelectedServer: ServerConfig?): Int { + private fun importConfig( + str: String?, + subid: String, + removedSelectedServer: ServerConfig? + ): Int { try { if (str == null || TextUtils.isEmpty(str)) { return R.string.toast_none_data } //maybe sub - if (TextUtils.isEmpty(subid) && (str.startsWith(HTTP_PROTOCOL) || str.startsWith(HTTPS_PROTOCOL))) { + if (TextUtils.isEmpty(subid) && (str.startsWith(HTTP_PROTOCOL) || str.startsWith( + HTTPS_PROTOCOL + )) + ) { MmkvManager.importUrlAsSubscription(str) return 0 } @@ -201,9 +241,9 @@ object AngConfigManager { val vmessQRCode = Gson().fromJson(result, VmessQRCode::class.java) // Although VmessQRCode fields are non null, looks like Gson may still create null fields if (TextUtils.isEmpty(vmessQRCode.add) - || TextUtils.isEmpty(vmessQRCode.port) - || TextUtils.isEmpty(vmessQRCode.id) - || TextUtils.isEmpty(vmessQRCode.net) + || TextUtils.isEmpty(vmessQRCode.port) + || TextUtils.isEmpty(vmessQRCode.id) + || TextUtils.isEmpty(vmessQRCode.net) ) { return R.string.toast_incorrect_protocol } @@ -213,16 +253,28 @@ object AngConfigManager { vnext.address = vmessQRCode.add vnext.port = Utils.parseInt(vmessQRCode.port) vnext.users[0].id = vmessQRCode.id - vnext.users[0].security = if (TextUtils.isEmpty(vmessQRCode.scy)) DEFAULT_SECURITY else vmessQRCode.scy + vnext.users[0].security = + if (TextUtils.isEmpty(vmessQRCode.scy)) DEFAULT_SECURITY else vmessQRCode.scy vnext.users[0].alterId = Utils.parseInt(vmessQRCode.aid) } - val sni = streamSetting.populateTransportSettings(vmessQRCode.net, vmessQRCode.type, vmessQRCode.host, - vmessQRCode.path, vmessQRCode.path, vmessQRCode.host, vmessQRCode.path, vmessQRCode.type, vmessQRCode.path) + val sni = streamSetting.populateTransportSettings( + vmessQRCode.net, + vmessQRCode.type, + vmessQRCode.host, + vmessQRCode.path, + vmessQRCode.path, + vmessQRCode.host, + vmessQRCode.path, + vmessQRCode.type, + vmessQRCode.path + ) val fingerprint = vmessQRCode.fp ?: streamSetting.tlsSettings?.fingerprint - streamSetting.populateTlsSettings(vmessQRCode.tls, allowInsecure, - if (TextUtils.isEmpty(vmessQRCode.sni)) sni else vmessQRCode.sni, - fingerprint, vmessQRCode.alpn, null, null, null) + streamSetting.populateTlsSettings( + vmessQRCode.tls, allowInsecure, + if (TextUtils.isEmpty(vmessQRCode.sni)) sni else vmessQRCode.sni, + fingerprint, vmessQRCode.alpn, null, null, null + ) } } } else if (str.startsWith(EConfigType.SHADOWSOCKS.protocolScheme)) { @@ -232,7 +284,8 @@ object AngConfigManager { val indexSplit = result.indexOf("#") if (indexSplit > 0) { try { - config.remarks = Utils.urlDecode(result.substring(indexSplit + 1, result.length)) + config.remarks = + Utils.urlDecode(result.substring(indexSplit + 1, result.length)) } catch (e: Exception) { e.printStackTrace() } @@ -243,13 +296,17 @@ object AngConfigManager { //part decode val indexS = result.indexOf("@") result = if (indexS > 0) { - Utils.decode(result.substring(0, indexS)) + result.substring(indexS, result.length) + Utils.decode(result.substring(0, indexS)) + result.substring( + indexS, + result.length + ) } else { Utils.decode(result) } val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)/?$".toRegex() - val match = legacyPattern.matchEntire(result) ?: return R.string.toast_incorrect_protocol + val match = legacyPattern.matchEntire(result) + ?: return R.string.toast_incorrect_protocol config.outboundBean?.settings?.servers?.get(0)?.let { server -> server.address = match.groupValues[3].removeSurrounding("[", "]") @@ -264,7 +321,8 @@ object AngConfigManager { config = ServerConfig.create(EConfigType.SOCKS) if (indexSplit > 0) { try { - config.remarks = Utils.urlDecode(result.substring(indexSplit + 1, result.length)) + config.remarks = + Utils.urlDecode(result.substring(indexSplit + 1, result.length)) } catch (e: Exception) { e.printStackTrace() } @@ -275,18 +333,23 @@ object AngConfigManager { //part decode val indexS = result.indexOf("@") if (indexS > 0) { - result = Utils.decode(result.substring(0, indexS)) + result.substring(indexS, result.length) + result = Utils.decode(result.substring(0, indexS)) + result.substring( + indexS, + result.length + ) } else { result = Utils.decode(result) } val legacyPattern = "^(.*):(.*)@(.+?):(\\d+?)$".toRegex() - val match = legacyPattern.matchEntire(result) ?: return R.string.toast_incorrect_protocol + val match = + legacyPattern.matchEntire(result) ?: return R.string.toast_incorrect_protocol config.outboundBean?.settings?.servers?.get(0)?.let { server -> server.address = match.groupValues[3].removeSurrounding("[", "]") server.port = match.groupValues[4].toInt() - val socksUsersBean = V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean() + val socksUsersBean = + V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean() socksUsersBean.user = match.groupValues[1].lowercase() socksUsersBean.pass = match.groupValues[2] server.users = listOf(socksUsersBean) @@ -302,17 +365,29 @@ object AngConfigManager { val queryParam = uri.rawQuery.split("&") .associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } } - val sni = config.outboundBean?.streamSettings?.populateTransportSettings(queryParam["type"] ?: "tcp", queryParam["headerType"], - queryParam["host"], queryParam["path"], queryParam["seed"], queryParam["quicSecurity"], queryParam["key"], - queryParam["mode"], queryParam["serviceName"]) + val sni = config.outboundBean?.streamSettings?.populateTransportSettings( + queryParam["type"] ?: "tcp", + queryParam["headerType"], + queryParam["host"], + queryParam["path"], + queryParam["seed"], + queryParam["quicSecurity"], + queryParam["key"], + queryParam["mode"], + queryParam["serviceName"] + ) fingerprint = queryParam["fp"] ?: "" - config.outboundBean?.streamSettings?.populateTlsSettings(queryParam["security"] ?: TLS, - allowInsecure, queryParam["sni"] ?: sni!!, fingerprint, queryParam["alpn"], - null, null, null) + config.outboundBean?.streamSettings?.populateTlsSettings( + queryParam["security"] ?: TLS, + allowInsecure, queryParam["sni"] ?: sni!!, fingerprint, queryParam["alpn"], + null, null, null + ) flow = queryParam["flow"] ?: "" } else { - config.outboundBean?.streamSettings?.populateTlsSettings(TLS, allowInsecure, "", - fingerprint, null, null, null, null) + config.outboundBean?.streamSettings?.populateTlsSettings( + TLS, allowInsecure, "", + fingerprint, null, null, null, null + ) } config.outboundBean?.settings?.servers?.get(0)?.let { server -> @@ -335,27 +410,41 @@ object AngConfigManager { vnext.port = uri.port vnext.users[0].id = uri.userInfo vnext.users[0].encryption = queryParam["encryption"] ?: "none" - vnext.users[0].flow =queryParam["flow"] ?: "" + vnext.users[0].flow = queryParam["flow"] ?: "" } - val sni = streamSetting.populateTransportSettings(queryParam["type"] ?: "tcp", queryParam["headerType"], - queryParam["host"], queryParam["path"], queryParam["seed"], queryParam["quicSecurity"], queryParam["key"], - queryParam["mode"], queryParam["serviceName"]) + val sni = streamSetting.populateTransportSettings( + queryParam["type"] ?: "tcp", + queryParam["headerType"], + queryParam["host"], + queryParam["path"], + queryParam["seed"], + queryParam["quicSecurity"], + queryParam["key"], + queryParam["mode"], + queryParam["serviceName"] + ) fingerprint = queryParam["fp"] ?: "" val pbk = queryParam["pbk"] ?: "" val sid = queryParam["sid"] ?: "" - val spx = Utils.urlDecode(queryParam["spx"] ?: "") - streamSetting.populateTlsSettings(queryParam["security"] ?: "", allowInsecure, - queryParam["sni"] ?: sni, fingerprint, queryParam["alpn"], pbk, sid, spx) + val spx = Utils.urlDecode(queryParam["spx"] ?: "") + streamSetting.populateTlsSettings( + queryParam["security"] ?: "", allowInsecure, + queryParam["sni"] ?: sni, fingerprint, queryParam["alpn"], pbk, sid, spx + ) } - if (config == null){ + if (config == null) { return R.string.toast_incorrect_protocol } config.subscriptionId = subid val guid = MmkvManager.encodeServerConfig("", config) if (removedSelectedServer != null && - config.getProxyOutbound()?.getServerAddress() == removedSelectedServer.getProxyOutbound()?.getServerAddress() && - config.getProxyOutbound()?.getServerPort() == removedSelectedServer.getProxyOutbound()?.getServerPort()) { + config.getProxyOutbound() + ?.getServerAddress() == removedSelectedServer.getProxyOutbound() + ?.getServerAddress() && + config.getProxyOutbound() + ?.getServerPort() == removedSelectedServer.getProxyOutbound()?.getServerPort() + ) { mainStorage?.encode(KEY_SELECTED_SERVER, guid) } } catch (e: Exception) { @@ -365,14 +454,18 @@ object AngConfigManager { return 0 } - private fun tryParseNewVmess(uriString: String, config: ServerConfig, allowInsecure: Boolean): Boolean { + private fun tryParseNewVmess( + uriString: String, + config: ServerConfig, + allowInsecure: Boolean + ): Boolean { return runCatching { val uri = URI(uriString) check(uri.scheme == "vmess") val (_, protocol, tlsStr, uuid, alterId) = - Regex("(tcp|http|ws|kcp|quic|grpc)(\\+tls)?:([0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12})") - .matchEntire(uri.userInfo)?.groupValues - ?: error("parse user info fail.") + Regex("(tcp|http|ws|kcp|quic|grpc)(\\+tls)?:([0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12})") + .matchEntire(uri.userInfo)?.groupValues + ?: error("parse user info fail.") val tls = tlsStr.isNotBlank() val queryParam = uri.rawQuery.split("&") .associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } } @@ -387,12 +480,19 @@ object AngConfigManager { vnext.users[0].alterId = alterId.toInt() } var fingerprint = streamSetting.tlsSettings?.fingerprint - val sni = streamSetting.populateTransportSettings(protocol, queryParam["type"], - queryParam["host"]?.split("|")?.get(0) ?: "", - queryParam["path"]?.takeIf { it.trim() != "/" } ?: "", queryParam["seed"], queryParam["security"], - queryParam["key"], queryParam["mode"], queryParam["serviceName"]) - streamSetting.populateTlsSettings(if (tls) TLS else "", allowInsecure, sni, fingerprint, null, - null, null, null) + val sni = streamSetting.populateTransportSettings(protocol, + queryParam["type"], + queryParam["host"]?.split("|")?.get(0) ?: "", + queryParam["path"]?.takeIf { it.trim() != "/" } ?: "", + queryParam["seed"], + queryParam["security"], + queryParam["key"], + queryParam["mode"], + queryParam["serviceName"]) + streamSetting.populateTlsSettings( + if (tls) TLS else "", allowInsecure, sni, fingerprint, null, + null, null, null + ) true }.getOrElse { false } } @@ -480,12 +580,16 @@ object AngConfigManager { vmessQRCode.add = outbound.getServerAddress().orEmpty() vmessQRCode.port = outbound.getServerPort().toString() vmessQRCode.id = outbound.getPassword().orEmpty() - vmessQRCode.aid = outbound.settings?.vnext?.get(0)?.users?.get(0)?.alterId.toString() - vmessQRCode.scy = outbound.settings?.vnext?.get(0)?.users?.get(0)?.security.toString() + vmessQRCode.aid = + outbound.settings?.vnext?.get(0)?.users?.get(0)?.alterId.toString() + vmessQRCode.scy = + outbound.settings?.vnext?.get(0)?.users?.get(0)?.security.toString() vmessQRCode.net = streamSetting.network vmessQRCode.tls = streamSetting.security vmessQRCode.sni = streamSetting.tlsSettings?.serverName.orEmpty() - vmessQRCode.alpn = Utils.removeWhiteSpace(streamSetting.tlsSettings?.alpn?.joinToString()).orEmpty() + vmessQRCode.alpn = + Utils.removeWhiteSpace(streamSetting.tlsSettings?.alpn?.joinToString()) + .orEmpty() vmessQRCode.fp = streamSetting.tlsSettings?.fingerprint.orEmpty() outbound.getTransportSettingDetails()?.let { transportDetails -> vmessQRCode.type = transportDetails[0] @@ -495,25 +599,34 @@ object AngConfigManager { val json = Gson().toJson(vmessQRCode) Utils.encode(json) } + EConfigType.CUSTOM, EConfigType.WIREGUARD -> "" EConfigType.SHADOWSOCKS -> { val remark = "#" + Utils.urlEncode(config.remarks) - val pw = Utils.encode("${outbound.getSecurityEncryption()}:${outbound.getPassword()}") - val url = String.format("%s@%s:%s", - pw, - Utils.getIpv6Address(outbound.getServerAddress()!!), - outbound.getServerPort()) - url + remark - } - EConfigType.SOCKS -> { - val remark = "#" + Utils.urlEncode(config.remarks) - val pw = Utils.encode("${outbound.settings?.servers?.get(0)?.users?.get(0)?.user}:${outbound.getPassword()}") - val url = String.format("%s@%s:%s", + val pw = + Utils.encode("${outbound.getSecurityEncryption()}:${outbound.getPassword()}") + val url = String.format( + "%s@%s:%s", pw, Utils.getIpv6Address(outbound.getServerAddress()!!), - outbound.getServerPort()) + outbound.getServerPort() + ) url + remark } + + EConfigType.SOCKS -> { + val remark = "#" + Utils.urlEncode(config.remarks) + val pw = + Utils.encode("${outbound.settings?.servers?.get(0)?.users?.get(0)?.user}:${outbound.getPassword()}") + val url = String.format( + "%s@%s:%s", + pw, + Utils.getIpv6Address(outbound.getServerAddress()!!), + outbound.getServerPort() + ) + url + remark + } + EConfigType.VLESS, EConfigType.TROJAN -> { val remark = "#" + Utils.urlEncode(config.remarks) @@ -537,12 +650,14 @@ object AngConfigManager { } dicQuery["security"] = streamSetting.security.ifEmpty { "none" } - (streamSetting.tlsSettings?: streamSetting.realitySettings)?.let { tlsSetting -> + (streamSetting.tlsSettings + ?: streamSetting.realitySettings)?.let { tlsSetting -> if (!TextUtils.isEmpty(tlsSetting.serverName)) { dicQuery["sni"] = tlsSetting.serverName } if (!tlsSetting.alpn.isNullOrEmpty() && tlsSetting.alpn.isNotEmpty()) { - dicQuery["alpn"] = Utils.removeWhiteSpace(tlsSetting.alpn.joinToString()).orEmpty() + dicQuery["alpn"] = + Utils.removeWhiteSpace(tlsSetting.alpn.joinToString()).orEmpty() } if (!TextUtils.isEmpty(tlsSetting.fingerprint)) { dicQuery["fp"] = tlsSetting.fingerprint!! @@ -567,12 +682,14 @@ object AngConfigManager { dicQuery["host"] = Utils.urlEncode(transportDetails[1]) } } + "kcp" -> { dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" } if (!TextUtils.isEmpty(transportDetails[2])) { dicQuery["seed"] = Utils.urlEncode(transportDetails[2]) } } + "ws" -> { if (!TextUtils.isEmpty(transportDetails[1])) { dicQuery["host"] = Utils.urlEncode(transportDetails[1]) @@ -581,6 +698,7 @@ object AngConfigManager { dicQuery["path"] = Utils.urlEncode(transportDetails[2]) } } + "http", "h2" -> { dicQuery["type"] = "http" if (!TextUtils.isEmpty(transportDetails[1])) { @@ -590,11 +708,13 @@ object AngConfigManager { dicQuery["path"] = Utils.urlEncode(transportDetails[2]) } } + "quic" -> { dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" } dicQuery["quicSecurity"] = Utils.urlEncode(transportDetails[1]) dicQuery["key"] = Utils.urlEncode(transportDetails[2]) } + "grpc" -> { dicQuery["mode"] = transportDetails[0] dicQuery["serviceName"] = transportDetails[2] @@ -602,13 +722,15 @@ object AngConfigManager { } } val query = "?" + dicQuery.toList().joinToString( - separator = "&", - transform = { it.first + "=" + it.second }) + separator = "&", + transform = { it.first + "=" + it.second }) - val url = String.format("%s@%s:%s", - outbound.getPassword(), - Utils.getIpv6Address(outbound.getServerAddress()!!), - outbound.getServerPort()) + val url = String.format( + "%s@%s:%s", + outbound.getPassword(), + Utils.getIpv6Address(outbound.getServerAddress()!!), + outbound.getServerPort() + ) url + query + remark } } @@ -756,17 +878,34 @@ object AngConfigManager { var count = 0 servers.lines() - .reversed() - .forEach { - val resId = importConfig(it, subid, removedSelectedServer) - if (resId == 0) { - count++ - } + .reversed() + .forEach { + val resId = importConfig(it, subid, removedSelectedServer) + if (resId == 0) { + count++ } + } return count } catch (e: Exception) { e.printStackTrace() } return 0 } + + fun importSubscription(remark: String, url: String, enabled: Boolean = true): Boolean { + val subId = Utils.getUuid() + val subItem = SubscriptionItem() + + + subItem.remarks = remark + subItem.url = url + subItem.enabled = enabled + + if (TextUtils.isEmpty(subItem.remarks) || TextUtils.isEmpty(subItem.url)) { + return false + } + subStorage?.encode(subId, Gson().toJson(subItem)) + + return true + } } diff --git a/V2rayNG/app/src/main/res/values-fa/strings.xml b/V2rayNG/app/src/main/res/values-fa/strings.xml index f3779193..5397da42 100644 --- a/V2rayNG/app/src/main/res/values-fa/strings.xml +++ b/V2rayNG/app/src/main/res/values-fa/strings.xml @@ -234,5 +234,7 @@ VPN فقط پروکسی + اشتراک با موفقیت ذخیره شد + ذخیره اشتراک ناموفق بود diff --git a/V2rayNG/app/src/main/res/values-ru/strings.xml b/V2rayNG/app/src/main/res/values-ru/strings.xml index 41663951..05ac1688 100644 --- a/V2rayNG/app/src/main/res/values-ru/strings.xml +++ b/V2rayNG/app/src/main/res/values-ru/strings.xml @@ -239,5 +239,7 @@ VPN Только прокси + подписка успешно импортирована + не удалось импортировать подписку diff --git a/V2rayNG/app/src/main/res/values-zh-rCN/strings.xml b/V2rayNG/app/src/main/res/values-zh-rCN/strings.xml index 738ace60..c86b8a7c 100644 --- a/V2rayNG/app/src/main/res/values-zh-rCN/strings.xml +++ b/V2rayNG/app/src/main/res/values-zh-rCN/strings.xml @@ -234,5 +234,7 @@ VPN 仅代理 + 订阅导入成功 + 导入订阅失败 diff --git a/V2rayNG/app/src/main/res/values/strings.xml b/V2rayNG/app/src/main/res/values/strings.xml index 6a539045..eb3c42ab 100644 --- a/V2rayNG/app/src/main/res/values/strings.xml +++ b/V2rayNG/app/src/main/res/values/strings.xml @@ -212,6 +212,9 @@ Connected, tap to check connection Not connected + Subscription imported Successfully + Import subscription failed + QRcode Export to clipboard