NekoX/TMessagesProj/src/main/java/tw/nekomimi/nekogram/utils/ProxyUtil.kt

548 lines
16 KiB
Kotlin

package tw.nekomimi.nekogram.utils
import android.Manifest
import android.app.Activity
import android.app.AlertDialog
import android.content.ClipboardManager
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.os.Build
import android.os.Environment
import android.util.Base64
import android.view.Gravity
import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.Toast
import androidx.core.view.setPadding
import com.google.zxing.*
import com.google.zxing.common.GlobalHistogramBinarizer
import com.google.zxing.qrcode.QRCodeReader
import com.google.zxing.qrcode.QRCodeWriter
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import com.v2ray.ang.V2RayConfig.SSR_PROTOCOL
import com.v2ray.ang.V2RayConfig.SS_PROTOCOL
import com.v2ray.ang.V2RayConfig.TROJAN_PROTOCOL
import com.v2ray.ang.V2RayConfig.VMESS1_PROTOCOL
import com.v2ray.ang.V2RayConfig.VMESS_PROTOCOL
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.telegram.messenger.*
import org.telegram.messenger.browser.Browser
import tw.nekomimi.nekogram.BottomBuilder
import tw.nekomimi.nekogram.utils.AlertUtil.showToast
import java.io.File
import java.net.NetworkInterface
import java.util.*
import kotlin.collections.HashMap
object ProxyUtil {
@JvmStatic
fun isVPNEnabled(): Boolean {
val networkList = mutableListOf<String>()
runCatching {
Collections.list(NetworkInterface.getNetworkInterfaces()).forEach {
if (it.isUp) networkList.add(it.name)
}
}
return networkList.contains("tun0")
}
@JvmStatic
fun parseProxies(_text: String): MutableList<String> {
val text = runCatching {
String(Base64.decode(_text, Base64.NO_PADDING))
}.recover {
_text
}.getOrThrow()
val proxies = mutableListOf<String>()
text.split('\n').map { it.split(" ") }.forEach {
it.forEach { line ->
if (line.startsWith("tg://proxy") ||
line.startsWith("tg://socks") ||
line.startsWith("https://t.me/proxy") ||
line.startsWith("https://t.me/socks") ||
line.startsWith(VMESS_PROTOCOL) ||
line.startsWith(VMESS1_PROTOCOL) ||
line.startsWith(SS_PROTOCOL) ||
line.startsWith(SSR_PROTOCOL) ||
line.startsWith(TROJAN_PROTOCOL) /*||
line.startsWith(RB_PROTOCOL)*/) {
runCatching { proxies.add(SharedConfig.parseProxyInfo(line).toUrl()) }
}
}
}
if (proxies.isEmpty()) error("no proxy link found")
return proxies
}
@JvmStatic
fun importFromClipboard(ctx: Activity) {
var text = (ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).primaryClip?.getItemAt(0)?.text?.toString()
val proxies = mutableListOf<SharedConfig.ProxyInfo>()
var error = false
text?.trim()?.split('\n')?.map { it.split(" ") }?.forEach {
it.forEach { line ->
if (line.startsWith("tg://proxy") ||
line.startsWith("tg://socks") ||
line.startsWith("https://t.me/proxy") ||
line.startsWith("https://t.me/socks") ||
line.startsWith(VMESS_PROTOCOL) ||
line.startsWith(VMESS1_PROTOCOL) ||
line.startsWith(SS_PROTOCOL) ||
line.startsWith(SSR_PROTOCOL) ||
line.startsWith(TROJAN_PROTOCOL) /*||
line.startsWith(RB_PROTOCOL)*/) {
runCatching { proxies.add(SharedConfig.parseProxyInfo(line)) }.onFailure {
error = true
showToast(LocaleController.getString("BrokenLink", R.string.BrokenLink) + ": ${it.message ?: it.javaClass.simpleName}")
}
}
}
}
runCatching {
if (proxies.isNullOrEmpty() && !error) {
String(Base64.decode(text, Base64.NO_PADDING)).trim().split('\n').map { it.split(" ") }.forEach { str ->
str.forEach { line ->
if (line.startsWith("tg://proxy") ||
line.startsWith("tg://socks") ||
line.startsWith("https://t.me/proxy") ||
line.startsWith("https://t.me/socks") ||
line.startsWith(VMESS_PROTOCOL) ||
line.startsWith(VMESS1_PROTOCOL) ||
line.startsWith(SS_PROTOCOL) ||
line.startsWith(SSR_PROTOCOL) ||
line.startsWith(TROJAN_PROTOCOL) /*||
line.startsWith(RB_PROTOCOL)*/) {
runCatching { proxies.add(SharedConfig.parseProxyInfo(line)) }.onFailure {
error = true
showToast(LocaleController.getString("BrokenLink", R.string.BrokenLink) + ": ${it.message ?: it.javaClass.simpleName}")
}
}
}
}
}
}
if (proxies.isNullOrEmpty()) {
if (!error) showToast(LocaleController.getString("BrokenLink", R.string.BrokenLink))
return
} else if (!error) {
AlertUtil.showSimpleAlert(ctx, LocaleController.getString("ImportedProxies", R.string.ImportedProxies) + "\n\n" + proxies.joinToString("\n") { it.title })
}
proxies.forEach {
SharedConfig.addProxy(it)
}
UIUtil.runOnUIThread {
NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.proxySettingsChanged)
}
}
@JvmStatic
fun importProxy(ctx: Context, link: String): Boolean {
runCatching {
if (link.startsWith(VMESS_PROTOCOL) || link.startsWith(VMESS1_PROTOCOL)) {
AndroidUtilities.showVmessAlert(ctx, SharedConfig.VmessProxy(link))
} else if (link.startsWith(TROJAN_PROTOCOL)) {
AndroidUtilities.showTrojanAlert(ctx, SharedConfig.VmessProxy(link))
} else if (link.startsWith(SS_PROTOCOL)) {
AndroidUtilities.showShadowsocksAlert(ctx, SharedConfig.ShadowsocksProxy(link))
} else if (link.startsWith(SSR_PROTOCOL)) {
AndroidUtilities.showShadowsocksRAlert(ctx, SharedConfig.ShadowsocksRProxy(link))
} else {
val url = link.replace("tg://", "https://t.me/").toHttpUrlOrNull()!!
AndroidUtilities.showProxyAlert(ctx,
url.queryParameter("server") ?: return false,
url.queryParameter("port") ?: return false,
url.queryParameter("user"),
url.queryParameter("pass"),
url.queryParameter("secret"),
url.fragment)
}
return true
}.onFailure {
FileLog.e(it)
if (BuildVars.LOGS_ENABLED) {
AlertUtil.showSimpleAlert(ctx, it)
} else {
showToast("${LocaleController.getString("BrokenLink", R.string.BrokenLink)}: ${it.message}")
}
}
return false
}
@JvmStatic
fun importInBackground(link: String): SharedConfig.ProxyInfo {
val info = runCatching {
if (link.startsWith(VMESS_PROTOCOL) || link.startsWith(VMESS1_PROTOCOL)) {
SharedConfig.VmessProxy(link)
} else if (link.startsWith(SS_PROTOCOL)) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
error(LocaleController.getString("MinApi21Required", R.string.MinApi21Required))
}
SharedConfig.ShadowsocksProxy(link)
} else if (link.startsWith(SSR_PROTOCOL)) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
error(LocaleController.getString("MinApi21Required", R.string.MinApi21Required))
}
SharedConfig.ShadowsocksRProxy(link)
} else {
SharedConfig.ProxyInfo.fromUrl(link)
}
}.getOrThrow()
if (!(SharedConfig.addProxy(info) === info)) {
error("already exists")
}
return info
}
@JvmStatic
fun shareProxy(ctx: Activity, info: SharedConfig.ProxyInfo, type: Int) {
val url = info.toUrl();
if (type == 1) {
AndroidUtilities.addToClipboard(url)
Toast.makeText(ctx, LocaleController.getString("LinkCopied", R.string.LinkCopied), Toast.LENGTH_LONG).show()
} else if (type == 0) {
val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.type = "text/plain"
shareIntent.putExtra(Intent.EXTRA_TEXT, url)
val chooserIntent = Intent.createChooser(shareIntent, LocaleController.getString("ShareLink", R.string.ShareLink))
chooserIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
ctx.startActivity(chooserIntent)
} else {
showQrDialog(ctx, url)
}
}
@JvmStatic
fun getOwnerActivity(ctx: Context): Activity {
if (ctx is Activity) return ctx
if (ctx is ContextWrapper) return getOwnerActivity(ctx.baseContext)
error("unable cast ${ctx.javaClass.name} to activity")
}
@JvmStatic
@JvmOverloads
fun showQrDialog(ctx: Context, text: String, icon: ((Int) -> Bitmap)? = null): AlertDialog {
val code = createQRCode(text, icon = icon)
ctx.setTheme(R.style.Theme_TMessages)
return AlertDialog.Builder(ctx).setView(LinearLayout(ctx).apply {
gravity = Gravity.CENTER
setBackgroundColor(Color.TRANSPARENT)
addView(LinearLayout(ctx).apply {
val root = this
gravity = Gravity.CENTER
setBackgroundColor(Color.WHITE)
setPadding(AndroidUtilities.dp(16f))
val width = AndroidUtilities.dp(260f)
addView(ImageView(ctx).apply {
setImageBitmap(code)
scaleType = ImageView.ScaleType.FIT_XY
setOnLongClickListener {
val builder = BottomBuilder(ctx)
builder.addItems(arrayOf(
LocaleController.getString("SaveToGallery", R.string.SaveToGallery),
LocaleController.getString("Cancel", R.string.Cancel)
), intArrayOf(
R.drawable.baseline_image_24,
R.drawable.baseline_cancel_24
)) { i, _, _ ->
if (i == 0) {
if (Build.VERSION.SDK_INT >= 23 && ctx.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
getOwnerActivity(ctx).requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 4)
return@addItems
}
val saveTo = File(Environment.getExternalStorageDirectory(), "${Environment.DIRECTORY_PICTURES}/share_${text.hashCode()}.jpg")
saveTo.parentFile?.mkdirs()
runCatching {
saveTo.createNewFile()
saveTo.outputStream().use {
loadBitmapFromView(root).compress(Bitmap.CompressFormat.JPEG, 100, it);
}
AndroidUtilities.addMediaToGallery(saveTo.path)
showToast(LocaleController.getString("PhotoSavedHint", R.string.PhotoSavedHint))
}.onFailure {
FileLog.e(it)
showToast(it)
}
}
}
builder.show()
return@setOnLongClickListener true
}
}, LinearLayout.LayoutParams(width, width))
}, LinearLayout.LayoutParams(-2, -2).apply {
gravity = Gravity.CENTER
})
}).create().apply {
show()
window?.setBackgroundDrawableResource(android.R.color.transparent)
}
}
private fun loadBitmapFromView(v: View): Bitmap {
val b = Bitmap.createBitmap(v.width, v.height, Bitmap.Config.ARGB_8888)
val c = Canvas(b)
v.layout(v.left, v.top, v.right, v.bottom)
v.draw(c)
return b
}
@JvmStatic
fun createQRCode(text: String, size: Int = 768, icon: ((Int) -> Bitmap)? = null): Bitmap {
return try {
val hints = HashMap<EncodeHintType, Any>()
hints[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.M
QRCodeWriter().encode(text, BarcodeFormat.QR_CODE, size, size, hints, null, null, icon)
} catch (e: WriterException) {
FileLog.e(e);
Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
}
}
val qrReader = QRCodeReader()
@JvmStatic
fun tryReadQR(ctx: Activity, bitmap: Bitmap) {
val intArray = IntArray(bitmap.width * bitmap.height)
bitmap.getPixels(intArray, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height)
val source = RGBLuminanceSource(bitmap.width, bitmap.height, intArray)
try {
val result = qrReader.decode(BinaryBitmap(GlobalHistogramBinarizer(source)), mapOf(
DecodeHintType.TRY_HARDER to true
))
showLinkAlert(ctx, result.text)
} catch (e: Throwable) {
showToast(LocaleController.getString("NoQrFound", R.string.NoQrFound))
}
}
@JvmStatic
@JvmOverloads
fun showLinkAlert(ctx: Activity, text: String, tryInternal: Boolean = true) {
val builder = BottomBuilder(ctx)
if (tryInternal) {
runCatching {
if (Browser.isInternalUrl(text, booleanArrayOf(false))) {
Browser.openUrl(ctx, text)
return
}
}
}
builder.addTitle(text)
builder.addItems(arrayOf(
LocaleController.getString("Open", R.string.Open),
LocaleController.getString("Copy", R.string.Copy),
LocaleController.getString("ShareQRCode", R.string.ShareQRCode)
), intArrayOf(
R.drawable.baseline_open_in_browser_24,
R.drawable.baseline_content_copy_24,
R.drawable.wallet_qr
)) { which, _, _ ->
when (which) {
0 -> Browser.openUrl(ctx, text)
1 -> {
AndroidUtilities.addToClipboard(text)
showToast(LocaleController.getString("LinkCopied", R.string.LinkCopied))
}
else -> showQrDialog(ctx, text)
}
}
builder.show()
}
}