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() runCatching { Collections.list(NetworkInterface.getNetworkInterfaces()).forEach { if (it.isUp) networkList.add(it.name) } } return networkList.contains("tun0") } @JvmStatic fun parseProxies(_text: String): MutableList { val text = runCatching { String(Base64.decode(_text, Base64.NO_PADDING)) }.recover { _text }.getOrThrow() val proxies = mutableListOf() 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() 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() 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() } }