wip: sing-box module
|
@ -40,3 +40,6 @@
|
|||
[submodule "v2ray"]
|
||||
path = v2ray
|
||||
url = https://github.com/nekohasekai/AndroidLibV2rayLite
|
||||
[submodule "nekox-sing-box/sing-box"]
|
||||
path = nekox-sing-box/sing-box
|
||||
url = https://github.com/SagerNet/sing-box.git
|
|
@ -0,0 +1,53 @@
|
|||
package tw.nekomimi.nekogram.proxynext
|
||||
|
||||
import android.widget.Toast
|
||||
import org.json.JSONObject
|
||||
import org.telegram.messenger.ApplicationLoader
|
||||
import org.telegram.messenger.FileLog
|
||||
import org.telegram.messenger.LocaleController
|
||||
import org.telegram.messenger.R
|
||||
import java.lang.Exception
|
||||
|
||||
object ProxyConfig {
|
||||
@JvmStatic
|
||||
fun parseSingBoxConfig(url: String): BoxProxy? {
|
||||
try {
|
||||
if (url.startsWith(VMESS_PROTOCOL) || url.startsWith(VMESS1_PROTOCOL)) {
|
||||
return VMessBean().also { it.parseFromLink(url) }
|
||||
} else if (url.startsWith(SS_PROTOCOL)) {
|
||||
return ShadowsocksBean().also { it.parseFromLink(url) }
|
||||
} else if (url.startsWith(SSR_PROTOCOL)) {
|
||||
return ShadowsocksRBean().also { it.parseFromLink(url) }
|
||||
} else if (url.startsWith(TROJAN_PROTOCOL)) {
|
||||
return TrojanBean().also { it.parseFromLink(url) }
|
||||
}
|
||||
return null
|
||||
} catch (ex: Exception) {
|
||||
FileLog.e(ex);
|
||||
Toast.makeText(ApplicationLoader.applicationContext,
|
||||
LocaleController.getString("UnsupportedProxy", R.string.UnsupportedProxy),
|
||||
Toast.LENGTH_LONG).show()
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const val VMESS_PROTOCOL: String = "vmess://"
|
||||
const val VMESS1_PROTOCOL = "vmess1://"
|
||||
const val SS_PROTOCOL: String = "ss://"
|
||||
const val SSR_PROTOCOL: String = "ssr://"
|
||||
const val TROJAN_PROTOCOL: String = "trojan://"
|
||||
const val WS_PROTOCOL: String = "ws://"
|
||||
const val WSS_PROTOCOL: String = "wss://"
|
||||
|
||||
val SUPPORTED_PROTOCOLS = listOf(VMESS_PROTOCOL, VMESS1_PROTOCOL, SS_PROTOCOL, SSR_PROTOCOL, TROJAN_PROTOCOL)
|
||||
|
||||
abstract class BoxProxy {
|
||||
var socks5Port: Int = 1080
|
||||
|
||||
abstract fun parseFromLink(link: String)
|
||||
abstract fun parseFromBoxConf(json: JSONObject)
|
||||
abstract fun generateBoxConf(): JSONObject
|
||||
abstract fun generateLink(): String
|
||||
abstract fun getAddress(): String
|
||||
}
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
package tw.nekomimi.nekogram.proxynext
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import cn.hutool.core.codec.Base64
|
||||
import com.github.shadowsocks.plugin.PluginConfiguration
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import org.json.JSONObject
|
||||
|
||||
data class ShadowsocksBean(
|
||||
var host: String = "",
|
||||
var port: Int = 443,
|
||||
var password: String = "",
|
||||
var method: String = "aes-256-cfb",
|
||||
var plugin: String = "",
|
||||
var remarks: String = "",
|
||||
val pluginOptions: MutableMap<String, String> = HashMap()
|
||||
) : ProxyConfig.BoxProxy() {
|
||||
|
||||
companion object {
|
||||
val methods = arrayOf(
|
||||
"none",
|
||||
"rc4-md5",
|
||||
"aes-128-cfb",
|
||||
"aes-192-cfb",
|
||||
"aes-256-cfb",
|
||||
"aes-128-ctr",
|
||||
"aes-192-ctr",
|
||||
"aes-256-ctr",
|
||||
"bf-cfb",
|
||||
"camellia-128-cfb",
|
||||
"camellia-192-cfb",
|
||||
"camellia-256-cfb",
|
||||
"salsa20",
|
||||
"chacha20",
|
||||
"chacha20-ietf",
|
||||
"aes-128-gcm",
|
||||
"aes-192-gcm",
|
||||
"aes-256-gcm",
|
||||
"chacha20-ietf-poly1305",
|
||||
"xchacha20-ietf-poly1305"
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
override fun parseFromLink(link: String) {
|
||||
if (link.contains("@")) {
|
||||
// ss-android style
|
||||
val link = link.replace(ProxyConfig.SS_PROTOCOL, "https://").toHttpUrlOrNull()
|
||||
?: error("invalid ss-android link $link")
|
||||
|
||||
if (link.password.isNotBlank()) {
|
||||
host = link.host
|
||||
port = link.port
|
||||
password = link.password
|
||||
method = link.username
|
||||
plugin = link.queryParameter("plugin") ?: ""
|
||||
remarks = link.fragment ?: ""
|
||||
} else {
|
||||
val methodAndPswd = Base64.decodeStr(link.username)
|
||||
host = link.host
|
||||
port = link.port
|
||||
password = methodAndPswd.substringAfter(":")
|
||||
method = methodAndPswd.substringBefore(":")
|
||||
plugin = link.queryParameter("plugin") ?: ""
|
||||
remarks = link.fragment ?: ""
|
||||
}
|
||||
} else {
|
||||
// v2rayNG style
|
||||
var v2Url = link
|
||||
if (v2Url.contains("#")) v2Url = v2Url.substringBefore("#")
|
||||
val link = ("https://" + Base64.decodeStr(v2Url.substringAfter(ProxyConfig.SS_PROTOCOL))).toHttpUrlOrNull()
|
||||
?: error("invalid v2rayNG link $link")
|
||||
host = link.host
|
||||
port = link.port
|
||||
password = link.password
|
||||
method = link.username
|
||||
plugin = ""
|
||||
remarks = link.fragment ?: ""
|
||||
}
|
||||
// init
|
||||
if (method == "plain") method = "none"
|
||||
// resole plugin
|
||||
val pl = PluginConfiguration(plugin)
|
||||
|
||||
if (pl.selected.contains("v2ray") && pl.selected != "v2ray-plugin") {
|
||||
// v2ray plugin
|
||||
// pl.pluginsOptions["v2ray-plugin"] = pl.getOptions().apply { id = "v2ray-plugin" }
|
||||
// pl.pluginsOptions.remove(pl.selected)
|
||||
this.plugin = "v2ray-plugin"
|
||||
pl.pluginsOptions["v2ray-plugin"] = pl.getOptions().apply { id = "v2ray-plugin" }
|
||||
pl.getOptions().forEach { key, value ->
|
||||
run {
|
||||
if (value != null)
|
||||
this.pluginOptions[key] = value
|
||||
}
|
||||
}
|
||||
} else if (pl.selected == "obfs") {
|
||||
this.plugin = "obfs-local"
|
||||
pl.pluginsOptions["obfs-local"] = pl.getOptions().apply { id = "obfs-local" }
|
||||
pl.getOptions().forEach { key, value ->
|
||||
run {
|
||||
if (value != null)
|
||||
this.pluginOptions[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun parseFromBoxConf(json: JSONObject) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun generateBoxConf(): JSONObject {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun generateLink(): String {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun getAddress(): String {
|
||||
return this.host
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
package tw.nekomimi.nekogram.proxynext
|
||||
|
||||
import cn.hutool.core.codec.Base64
|
||||
import com.google.gson.JsonObject
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import org.json.JSONObject
|
||||
import tw.nekomimi.nekogram.proxynext.Utils.parseInt
|
||||
|
||||
data class ShadowsocksRBean(
|
||||
var host: String = "",
|
||||
var port: Int = 443,
|
||||
var method: String = "aes-256-cfb",
|
||||
var password: String = "",
|
||||
var protocol: String = "origin",
|
||||
var protocol_param: String = "",
|
||||
var obfs: String = "plain",
|
||||
var obfs_param: String = "",
|
||||
var remarks: String = "shadowsocksr"
|
||||
) : ProxyConfig.BoxProxy() {
|
||||
|
||||
override fun parseFromLink(link: String) {
|
||||
val params = Base64.decodeStr(link.substringAfter(ProxyConfig.SSR_PROTOCOL)).split(":")
|
||||
|
||||
host = params[0]
|
||||
port = params[1].parseInt()
|
||||
protocol = params[2]
|
||||
method = params[3]
|
||||
obfs = params[4]
|
||||
password = Base64.decodeStr(params[5].substringBefore("/"))
|
||||
|
||||
val httpUrl = ("https://localhost" + params[5].substringAfter("/")).toHttpUrl()
|
||||
|
||||
runCatching {
|
||||
obfs_param = Base64.decodeStr(httpUrl.queryParameter("obfsparam")!!)
|
||||
}
|
||||
runCatching {
|
||||
protocol_param = Base64.decodeStr(httpUrl.queryParameter("protoparam")!!)
|
||||
}
|
||||
runCatching {
|
||||
val remarks = httpUrl.queryParameter("remarks")
|
||||
if (remarks?.isNotBlank() == true) {
|
||||
this.remarks = Base64.decodeStr(remarks)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun parseFromBoxConf(json: JSONObject) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun generateBoxConf(): JSONObject {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun generateLink(): String {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun getAddress(): String {
|
||||
return this.host
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val methods = arrayOf(
|
||||
|
||||
"none",
|
||||
"table",
|
||||
"rc4",
|
||||
"rc4-md5",
|
||||
"rc4-md5-6",
|
||||
"aes-128-cfb",
|
||||
"aes-192-cfb",
|
||||
"aes-256-cfb",
|
||||
"aes-128-ctr",
|
||||
"aes-192-ctr",
|
||||
"aes-256-ctr",
|
||||
"bf-cfb",
|
||||
"camellia-128-cfb",
|
||||
"camellia-192-cfb",
|
||||
"camellia-256-cfb",
|
||||
"salsa20",
|
||||
"chacha20",
|
||||
"chacha20-ietf"
|
||||
|
||||
)
|
||||
|
||||
val protocols = arrayOf(
|
||||
"origin",
|
||||
"verify_simple",
|
||||
"verify_sha1",
|
||||
"auth_sha1",
|
||||
"auth_sha1_v2",
|
||||
"auth_sha1_v4",
|
||||
"auth_aes128_sha1",
|
||||
"auth_aes128_md5",
|
||||
"auth_chain_a",
|
||||
"auth_chain_b"
|
||||
)
|
||||
|
||||
val obfses = arrayOf(
|
||||
"plain",
|
||||
"http_simple",
|
||||
"http_post",
|
||||
"tls_simple",
|
||||
"tls1.2_ticket_auth"
|
||||
)
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
package tw.nekomimi.nekogram.proxynext
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ResolveInfo
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import org.telegram.messenger.ApplicationLoader
|
||||
import org.telegram.messenger.FileLog
|
||||
import tw.nekomimi.nekogram.proxy.GuardedProcessPool
|
||||
import tw.nekomimi.nekogram.proxy.ProxyManager
|
||||
import tw.nekomimi.nekogram.utils.ProxyUtil
|
||||
import java.io.File
|
||||
|
||||
class SingProxyManager private constructor() {
|
||||
|
||||
companion object {
|
||||
@JvmField
|
||||
val INSTANCE = SingProxyManager()
|
||||
|
||||
@JvmField
|
||||
val TEST_INSTANCE = SingProxyManager()
|
||||
}
|
||||
|
||||
val proxies = mutableListOf<ProxyConfig.BoxProxy>()
|
||||
private val allocatedPorts = mutableSetOf<Int>()
|
||||
|
||||
val isSingExist: Boolean by lazy {
|
||||
resolveProviders(ApplicationLoader.applicationContext).size == 1
|
||||
}
|
||||
|
||||
private val singPath: String? by lazy {
|
||||
val providers = resolveProviders(ApplicationLoader.applicationContext)
|
||||
val provider = providers.single().providerInfo
|
||||
provider?.metaData?.getString("nekox.messenger.sing.executable_path")
|
||||
?.let { relativePath ->
|
||||
File(provider.applicationInfo.nativeLibraryDir).resolve(relativePath).apply {
|
||||
}.absolutePath
|
||||
}
|
||||
}
|
||||
|
||||
private val singRunner: GuardedProcessPool = GuardedProcessPool {
|
||||
FileLog.e(it.toString())
|
||||
}
|
||||
|
||||
@Volatile
|
||||
var isStarted: Boolean = false
|
||||
|
||||
fun start() {
|
||||
synchronized(this) {
|
||||
if (isStarted) return
|
||||
isStarted = true
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
|
||||
}
|
||||
|
||||
fun restart() {
|
||||
|
||||
}
|
||||
|
||||
private fun resolveProviders(context: Context): List<ResolveInfo> {
|
||||
val uri = Uri.Builder()
|
||||
.scheme("plugin")
|
||||
.authority("nekox.messenger.sing")
|
||||
.path("/sing-box")
|
||||
.build()
|
||||
val flags = PackageManager.GET_META_DATA
|
||||
return context.packageManager.queryIntentContentProviders(
|
||||
Intent("nekox.messenger.sing.ACTION_SING_PLUGIN", uri), flags
|
||||
).filter { it.providerInfo.exported }
|
||||
}
|
||||
|
||||
fun setProxyRemarks(proxy: ProxyConfig.BoxProxy, remarks: String) {
|
||||
|
||||
}
|
||||
|
||||
fun allocatePort(proxy: ProxyConfig.BoxProxy): Int {
|
||||
var port = ProxyManager.mkPort()
|
||||
while (allocatedPorts.contains(port)) port = ProxyManager.mkPort()
|
||||
allocatedPorts.add(port)
|
||||
proxy.socks5Port = port
|
||||
return port
|
||||
}
|
||||
|
||||
fun addProxy(boxProxy: ProxyConfig.BoxProxy) {
|
||||
this.proxies.add(boxProxy)
|
||||
}
|
||||
|
||||
fun clearProxies() {
|
||||
check(!isStarted)
|
||||
this.proxies.clear()
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package tw.nekomimi.nekogram.proxynext
|
||||
|
||||
import java.net.URLDecoder
|
||||
|
||||
object Utils {
|
||||
fun String.parseInt(): Int {
|
||||
return Integer.parseInt(this)
|
||||
}
|
||||
|
||||
fun String.urlDecode(): String {
|
||||
return try {
|
||||
URLDecoder.decode(this, "UTF-8")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
this
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,291 @@
|
|||
package tw.nekomimi.nekogram.proxynext
|
||||
|
||||
import cn.hutool.core.codec.Base64
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonObject
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import org.json.JSONObject
|
||||
import tw.nekomimi.nekogram.proxynext.Utils.parseInt
|
||||
import tw.nekomimi.nekogram.proxynext.Utils.urlDecode
|
||||
|
||||
/*
|
||||
VMess / Trojan
|
||||
*/
|
||||
|
||||
data class VMessQRCode(var v: String = "",
|
||||
var ps: String = "",
|
||||
var add: String = "",
|
||||
var port: String = "",
|
||||
var id: String = "",
|
||||
var aid: String = "",
|
||||
var net: String = "",
|
||||
var type: String = "",
|
||||
var host: String = "",
|
||||
var path: String = "",
|
||||
var tls: String = "") {}
|
||||
|
||||
data class VMessBean(var uuid: String = "123456",
|
||||
var address: String = "",
|
||||
var port: Int = 443,
|
||||
var id: String = "",
|
||||
var alterId: Int = 0,
|
||||
var security: String = "auto",
|
||||
var network: String = "tcp",
|
||||
var remarks: String = "",
|
||||
var headerType: String = "none",
|
||||
var requestHost: String = "",
|
||||
var path: String = "",
|
||||
var streamSecurity: String = "",
|
||||
var configVersion: Int = 2,
|
||||
var testResult: String = "") : ProxyConfig.BoxProxy() {
|
||||
|
||||
override fun parseFromLink(link: String) {
|
||||
check(link.startsWith("vmess://") || link.startsWith("vmess1://"))
|
||||
try {
|
||||
if (link.isBlank()) error("empty link")
|
||||
if (link.startsWith(ProxyConfig.VMESS_PROTOCOL)) {
|
||||
val indexSplit = link.indexOf("?")
|
||||
if (indexSplit > 0) {
|
||||
resolveSimpleVmess1(link)
|
||||
} else {
|
||||
var result = link.replace(ProxyConfig.VMESS_PROTOCOL, "")
|
||||
result = Base64.decodeStr(result)
|
||||
if (result.isBlank()) {
|
||||
error("invalid url format")
|
||||
}
|
||||
|
||||
if (result.contains("= vmess")) {
|
||||
resolveSomeIOSAppShitCsvLink(result)
|
||||
} else {
|
||||
val vmessQRCode = Gson().fromJson(result, VMessQRCode::class.java)
|
||||
if (vmessQRCode.add.isBlank() || vmessQRCode.port.isBlank()
|
||||
|| vmessQRCode.id.isBlank() || vmessQRCode.aid.isBlank()
|
||||
|| vmessQRCode.net.isBlank())
|
||||
error("invalid vmess protocol")
|
||||
|
||||
security = "auto"
|
||||
network = "tcp"
|
||||
headerType = "none"
|
||||
|
||||
configVersion = vmessQRCode.v.parseInt()
|
||||
remarks = vmessQRCode.ps
|
||||
address = vmessQRCode.add
|
||||
port = vmessQRCode.port.parseInt()
|
||||
id = vmessQRCode.id
|
||||
alterId = vmessQRCode.aid.parseInt()
|
||||
network = vmessQRCode.net
|
||||
headerType = vmessQRCode.type
|
||||
requestHost = vmessQRCode.host
|
||||
path = vmessQRCode.path
|
||||
streamSecurity = vmessQRCode.tls
|
||||
}
|
||||
}
|
||||
upgradeServerVersion()
|
||||
} else if (link.startsWith(ProxyConfig.VMESS1_PROTOCOL)) {
|
||||
parseVmess1Link(link)
|
||||
} else {
|
||||
error("invalid protocol")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
throw IllegalArgumentException(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveSimpleVmess1(link: String) {
|
||||
var result = link.replace(ProxyConfig.VMESS_PROTOCOL, "")
|
||||
val indexSplit = result.indexOf("?")
|
||||
if (indexSplit > 0) {
|
||||
result = result.substring(0, indexSplit)
|
||||
}
|
||||
result = Base64.decodeStr(result)
|
||||
|
||||
val arr1 = result.split('@')
|
||||
if (arr1.count() != 2) {
|
||||
error("unexpected VMess1 link format")
|
||||
}
|
||||
val arr21 = arr1[0].split(':')
|
||||
val arr22 = arr1[1].split(':')
|
||||
if (arr21.count() != 2 || arr21.count() != 2) {
|
||||
error("unexpected VMess1 link format")
|
||||
}
|
||||
|
||||
address = arr22[0]
|
||||
port = arr22[1].parseInt()
|
||||
security = arr21[0]
|
||||
id = arr21[1]
|
||||
security = "chacha20-poly1305"
|
||||
network = "tcp"
|
||||
headerType = "none"
|
||||
remarks = ""
|
||||
alterId = 0
|
||||
}
|
||||
|
||||
private fun parseVmess1Link(link: String) {
|
||||
val lnk = ("https://" + link.substringAfter(ProxyConfig.VMESS1_PROTOCOL)).toHttpUrl()
|
||||
|
||||
address = lnk.host
|
||||
port = lnk.port
|
||||
id = lnk.username
|
||||
remarks = lnk.fragment ?: ""
|
||||
|
||||
lnk.queryParameterNames.forEach {
|
||||
when (it) {
|
||||
"tls" -> if (lnk.queryParameter(it) == "true") streamSecurity = "tls"
|
||||
"network" -> {
|
||||
network = lnk.queryParameter(it)!!
|
||||
if (network in arrayOf("http", "ws")) {
|
||||
path = lnk.encodedPath.urlDecode()
|
||||
}
|
||||
}
|
||||
"header" -> {
|
||||
headerType = lnk.queryParameter(it)!!
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveSomeIOSAppShitCsvLink(csv: String) {
|
||||
|
||||
val args = csv.split(",")
|
||||
address = args[1]
|
||||
port = args[2].toInt()
|
||||
security = args[3]
|
||||
id = args[4].replace("\"", "")
|
||||
args.subList(5, args.size).forEach {
|
||||
when {
|
||||
it == "over-tls=true" -> {
|
||||
streamSecurity = "tls"
|
||||
}
|
||||
it.startsWith("tls-host=") -> {
|
||||
requestHost = it.substringAfter("=")
|
||||
}
|
||||
it.startsWith("obfs=") -> {
|
||||
network = it.substringAfter("=")
|
||||
}
|
||||
|
||||
it.startsWith("obfs-path=") || it.contains("Host:") -> {
|
||||
runCatching {
|
||||
path = it
|
||||
.substringAfter("obfs-path=\"")
|
||||
.substringBefore("\"obfs")
|
||||
}
|
||||
runCatching {
|
||||
requestHost = it
|
||||
.substringAfter("Host:")
|
||||
.substringBefore("[")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun upgradeServerVersion(): Int {
|
||||
try {
|
||||
if (configVersion == 2) {
|
||||
return 0
|
||||
}
|
||||
|
||||
when (network) {
|
||||
"ws" -> {
|
||||
var path = ""
|
||||
var host = ""
|
||||
val lstParameter = requestHost.split(";")
|
||||
if (lstParameter.isNotEmpty()) {
|
||||
path = lstParameter[0].trim()
|
||||
}
|
||||
if (lstParameter.size > 1) {
|
||||
path = lstParameter[0].trim()
|
||||
host = lstParameter[1].trim()
|
||||
}
|
||||
this.path = path
|
||||
this.requestHost = host
|
||||
}
|
||||
"h2" -> {
|
||||
var path = ""
|
||||
var host = ""
|
||||
val lstParameter = requestHost.split(";")
|
||||
if (lstParameter.isNotEmpty()) {
|
||||
path = lstParameter[0].trim()
|
||||
}
|
||||
if (lstParameter.size > 1) {
|
||||
path = lstParameter[0].trim()
|
||||
host = lstParameter[1].trim()
|
||||
}
|
||||
this.path = path
|
||||
this.requestHost = host
|
||||
}
|
||||
}
|
||||
configVersion = 2
|
||||
return 0
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
override fun parseFromBoxConf(json: JSONObject) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun generateBoxConf(): JSONObject {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun generateLink(): String {
|
||||
val qr = VMessQRCode().also {
|
||||
it.v = configVersion.toString()
|
||||
it.ps = remarks
|
||||
it.add = address
|
||||
it.port = port.toString()
|
||||
it.id = id
|
||||
it.aid = alterId.toString()
|
||||
it.net = network
|
||||
it.type = headerType
|
||||
it.host = requestHost
|
||||
it.path = path
|
||||
it.tls = streamSecurity
|
||||
}
|
||||
return ProxyConfig.VMESS_PROTOCOL + Base64.encode(Gson().toJson(qr))
|
||||
}
|
||||
|
||||
override fun getAddress(): String {
|
||||
return this.address;
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as VMessBean
|
||||
|
||||
if (address != other.address) return false
|
||||
if (port != other.port) return false
|
||||
if (id != other.id) return false
|
||||
if (alterId != other.alterId) return false
|
||||
if (security != other.security) return false
|
||||
if (network != other.network) return false
|
||||
if (headerType != other.headerType) return false
|
||||
if (requestHost != other.requestHost) return false
|
||||
if (path != other.path) return false
|
||||
if (streamSecurity != other.streamSecurity) return false
|
||||
if (remarks != other.remarks) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = address.hashCode()
|
||||
result = 31 * result + port
|
||||
result = 31 * result + id.hashCode()
|
||||
result = 31 * result + alterId
|
||||
result = 31 * result + security.hashCode()
|
||||
result = 31 * result + network.hashCode()
|
||||
result = 31 * result + headerType.hashCode()
|
||||
result = 31 * result + requestHost.hashCode()
|
||||
result = 31 * result + path.hashCode()
|
||||
result = 31 * result + streamSecurity.hashCode()
|
||||
result = 31 * result + remarks.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
/build
|
|
@ -0,0 +1,43 @@
|
|||
#!/bin/bash
|
||||
|
||||
COMMIT_HASH=$1
|
||||
[[ -z "${ANDROID_NDK_HOME}" ]] && ANDROID_NDK_HOME="${ANDROID_HOME}/ndk/23.1.7779620"
|
||||
|
||||
echo "$COMMIT_HASH"
|
||||
git submodule init sing-box
|
||||
git submodule update sing-box
|
||||
|
||||
pushd sing-box || exit 1
|
||||
git fetch origin
|
||||
git reset --hard
|
||||
git clean -fdx
|
||||
git checkout "$COMMIT_HASH"
|
||||
|
||||
export PATH=$PATH:/usr/lib/go-1.19/bin/
|
||||
go version || exit 1
|
||||
|
||||
TOOLCHAIN="$(find "${ANDROID_NDK_HOME}"/toolchains/llvm/prebuilt/* -maxdepth 1 -type d -print -quit)/bin"
|
||||
ABIS=(armeabi-v7a arm64-v8a x86 x86_64)
|
||||
GO_ARCHS=('arm GOARM=7' arm64 386 amd64)
|
||||
CLANG_ARCHS=(armv7a-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android)
|
||||
|
||||
MIN_API="21"
|
||||
OUT_DIR="$(dirname "$(pwd)")/src/main/jniLibs"
|
||||
|
||||
mkdir -p "$OUT_DIR"
|
||||
|
||||
BIN="sing-box"
|
||||
for i in "${!ABIS[@]}"; do
|
||||
ABI="${ABIS[$i]}"
|
||||
[[ -f "${OUT_DIR}/${ABI}/${BIN}" ]] && continue
|
||||
echo "Build ${BIN} for ${ABI}"
|
||||
mkdir -p ${OUT_DIR}/${ABI}
|
||||
env \
|
||||
CGO_ENABLED=1 CC="${TOOLCHAIN}/${CLANG_ARCHS[$i]}${MIN_API}-clang" \
|
||||
GOOS=android GOARCH=${GO_ARCHS[$i]} \
|
||||
go build -v -trimpath -tags "-with_shadowsocksr " -ldflags "-s -w" ./cmd/sing-box || exit 1
|
||||
"${TOOLCHAIN}/llvm-strip" "sing-box" -o "${OUT_DIR}/${ABI}/${BIN}.so" || exit 1
|
||||
rm "sing-box" || exit 1
|
||||
done
|
||||
|
||||
popd || exit 1
|
|
@ -0,0 +1,102 @@
|
|||
import java.io.ByteArrayInputStream
|
||||
import java.util.Base64
|
||||
import java.util.Properties
|
||||
import org.apache.tools.ant.taskdefs.condition.Os
|
||||
import org.jetbrains.kotlin.scripting.compiler.plugin.impl.failure
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
fun setupSigningConfig() = Properties().apply {
|
||||
val base64: String? = System.getenv("LOCAL_PROPERTIES")
|
||||
if (!base64.isNullOrBlank()) {
|
||||
load(ByteArrayInputStream(Base64.getDecoder().decode(base64)))
|
||||
} else if (project.rootProject.file("local.properties").exists()) {
|
||||
load(project.rootProject.file("local.properties").inputStream())
|
||||
}
|
||||
if (getProperty("KEYSTORE_PASS")?.isBlank() == true) {
|
||||
setProperty("KEYSTORE_PASS", System.getenv("KEYSTORE_PASS"))
|
||||
setProperty("ALIAS_NAME", System.getenv("ALIAS_NAME"))
|
||||
setProperty("ALIAS_PASS", System.getenv("ALIAS_PASS"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
android {
|
||||
namespace = "nekox.messenger.sing"
|
||||
compileSdk = 33
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "nekox.messenger.sing"
|
||||
minSdk = 21
|
||||
targetSdk = 33
|
||||
versionCode = 100
|
||||
versionName = "1.0.0"
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
val releaseSigningKey = setupSigningConfig()
|
||||
if (!releaseSigningKey.getProperty("KEYSTORE_PASS").isNullOrBlank()){
|
||||
create("release") {
|
||||
storeFile = file("../TMessagesProj/release.keystore")
|
||||
storePassword = releaseSigningKey.getProperty("KEYSTORE_PASS")
|
||||
keyAlias = releaseSigningKey.getProperty("ALIAS_NAME")
|
||||
keyPassword = releaseSigningKey.getProperty("ALIAS_PASS")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// sourceSets["main"].jniLibs.srcDirs.plus(File(project.path, "libs"))
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
signingConfig = if(signingConfigs.names.contains("release")) signingConfigs.getByName("release") else signingConfigs.getByName("debug")
|
||||
}
|
||||
}
|
||||
splits {
|
||||
abi {
|
||||
isEnable = true
|
||||
isUniversalApk = true
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10")
|
||||
// implementation(fileTree("libs"))
|
||||
}
|
||||
|
||||
val singboxCommit = "ef73c6f2a9e5b40028d9bdc0c7e7023c32010fb1" // Release 1.1.6
|
||||
|
||||
val nativeBuild = task("native-build-sing-box") {
|
||||
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
|
||||
println("Build in Windows is not supported")
|
||||
} else {
|
||||
exec {
|
||||
workingDir(projectDir)
|
||||
executable("/bin/bash")
|
||||
args("build-sing-box", singboxCommit, android.defaultConfig.minSdkVersion)
|
||||
environment("ANDROID_HOME", android.sdkDirectory)
|
||||
environment("ANDROID_NDK_HOME", android.ndkDirectory)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.whenTaskAdded {
|
||||
if (name.contains("javaPreCompile")) {
|
||||
dependsOn(nativeBuild)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
|
@ -0,0 +1 @@
|
|||
Subproject commit ef73c6f2a9e5b40028d9bdc0c7e7023c32010fb1
|
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/nekox_singbox_plugin"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true">
|
||||
<provider
|
||||
android:authorities="nekox.messenger.sing.PluginProvider"
|
||||
android:name=".PluginProvider"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="nekox.messenger.sing.ACTION_SING_PLUGIN"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="nekox.messenger.sing.ACTION_SING_PLUGIN"/>
|
||||
<data android:scheme="plugin"
|
||||
android:host="nekox.messenger.sing"
|
||||
android:pathPrefix="/sing-box"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="nekox.messenger.sing.executable_path"
|
||||
android:value="sing-box.so"/>
|
||||
</provider>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -0,0 +1,45 @@
|
|||
package nekox.messenger.sing
|
||||
|
||||
import android.content.ContentProvider
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import android.database.MatrixCursor
|
||||
import android.net.Uri
|
||||
import android.os.ParcelFileDescriptor
|
||||
import java.io.File
|
||||
|
||||
class PluginProvider : ContentProvider() {
|
||||
override fun onCreate(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? {
|
||||
val result = MatrixCursor(projection)
|
||||
result.newRow().add("path", "sing-box")
|
||||
return result
|
||||
}
|
||||
|
||||
override fun getType(uri: Uri): String {
|
||||
return "application/x-elf"
|
||||
}
|
||||
|
||||
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
|
||||
check(mode == "r")
|
||||
var libraryPath = context?.applicationInfo?.nativeLibraryDir
|
||||
check(libraryPath != null && libraryPath != "")
|
||||
libraryPath += "/sing-box.so"
|
||||
return ParcelFileDescriptor.open(File(libraryPath), ParcelFileDescriptor.MODE_READ_ONLY)
|
||||
}
|
||||
|
||||
override fun insert(p0: Uri, p1: ContentValues?): Uri? {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun delete(p0: Uri, p1: String?, p2: Array<out String>?): Int {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun update(p0: Uri, p1: ContentValues?, p2: String?, p3: Array<out String>?): Int {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
|
@ -0,0 +1,170 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 982 B |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 5.8 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 7.6 KiB |
|
@ -0,0 +1,4 @@
|
|||
<resources>
|
||||
<string name="app_name">NekoX sing-box Plugin</string>
|
||||
<string name="nekox_singbox_plugin">NekoX sing-box Plugin</string>
|
||||
</resources>
|