package com.example.modifier

import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Build
import android.util.Log
import androidx.core.content.ContextCompat
import com.example.modifier.data.BackupItem
import com.example.modifier.data.BackupItemDao
import com.example.modifier.enums.RcsConfigureState
import com.example.modifier.http.KtorClient
import com.example.modifier.model.TelephonyConfig
import com.example.modifier.serializer.Json
import com.example.modifier.service.ModifierService
import com.example.modifier.ui.shellRun
import com.example.modifier.utils.RcsHackTool
import com.google.gson.Gson
import io.ktor.client.request.head
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.encodeToString
import org.apache.commons.io.FileUtils
import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.RandomStringUtils
import org.apache.commons.lang3.StringUtils
import java.io.File
import java.io.FileWriter
import java.nio.file.Files
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.util.Base64
import java.util.Date
import java.util.Locale
import kotlin.time.Duration.Companion.minutes

object Global {
    @JvmField
    var serverUrl: String = ""

    @JvmField
    var name: String? = ""

    @JvmField
    var telephonyConfig: TelephonyConfig = TelephonyConfig("", "", "", "", "", "", "", "")

    private const val TAG = "Modifier"

    @JvmStatic
    fun load() {
        val context = Utils.getContext()
        val prefs = context.getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE)
        serverUrl = prefs.getString("server", "http://47.98.225.28") ?: ""
        name = prefs.getString("name", Build.DEVICE)
        try {
            val file = File(ContextCompat.getDataDir(context), "config.json")
            if (file.exists()) {
                val gson = Gson()
                val json = FileUtils.readFileToString(file, "UTF-8")
                telephonyConfig = gson.fromJson(json, TelephonyConfig::class.java)
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    @JvmStatic
    val servers: MutableSet<String>
        get() {
            val context = Utils.getContext()
            val defServers: MutableSet<String> = HashSet()
            defServers.add("http://192.168.6.215:3000")
            defServers.add("http://192.168.50.135:3000")
            defServers.add("http://47.98.225.28")
            val prefs =
                context.getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE)
            return HashSet(prefs.getStringSet("servers", defServers))
        }

    @JvmStatic
    fun saveServer(server: String, name: String?) {
        serverUrl = server
        Global.name = name
        val context = Utils.getContext()
        val prefs = context.getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE)
        val servers = servers
        servers.add(server)
        prefs.edit().putStringSet("servers", servers)
            .putString("server", server)
            .putString("name", name)
            .apply()
    }

    @JvmStatic
    fun save() {
        val context = Utils.getContext()
        val file = File(ContextCompat.getDataDir(context), "config.json")
        FileUtils.write(file, Json.encodeToString(telephonyConfig), "UTF-8")
    }

    @JvmStatic
    fun save(telephonyConfig: TelephonyConfig, suspend: Boolean? = true) {
        Global.telephonyConfig.mcc = telephonyConfig.mcc
        Global.telephonyConfig.mnc = telephonyConfig.mnc
        Global.telephonyConfig.number = telephonyConfig.number
        Global.telephonyConfig.country = telephonyConfig.country
        Global.telephonyConfig.areaCode = telephonyConfig.areaCode
        Global.telephonyConfig.iccid = telephonyConfig.iccid
        Global.telephonyConfig.imei = telephonyConfig.imei
        Global.telephonyConfig.imsi = telephonyConfig.imsi
        Global.telephonyConfig.available = telephonyConfig.available

        try {
            if (suspend == true) {
                suspend(gms = true, sms = true)
            }
            save()

            Utils.runAsRoot(
                "setprop persist.spoof.mcc ${telephonyConfig.mcc}",
                "setprop persist.spoof.mnc ${telephonyConfig.mnc}",
                "setprop persist.spoof.number ${telephonyConfig.number}",
                "setprop persist.spoof.country ${telephonyConfig.country}",
                "setprop persist.spoof.iccid ${telephonyConfig.iccid}",
                "setprop persist.spoof.imei ${telephonyConfig.imei}",
                "setprop persist.spoof.imsi ${telephonyConfig.imsi}",
            )
            if (suspend == true) {
                unsuspend(gms = true, sms = true)
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    private fun isOldVersion(): Boolean {
        val info =
            Utils.getContext().packageManager.getPackageInfo("com.google.android.apps.messaging", 0)
        var oldVersion = false
        if (info != null) {
            if (info.versionCode < 170545910) {
                oldVersion = true
            }
        }
        return oldVersion
    }

    fun saveMock() {
        if (isOldVersion()) {
            val content = Utils.getContext().assets.open("us_numbers.txt").bufferedReader().use {
                it.readText()
            }
            // get random number
            content.split("\n")
                .filter { it.isNotBlank() }
                .shuffled()
                .firstOrNull()
                ?.let {
                    save(
                        TelephonyConfig(
                            number = it,
                            mcc = "310",
                            mnc = "150",
                            iccid = genICCID("310", "1"),
                            "310150" + RandomStringUtils.randomNumeric(9),
                            Utils.generateIMEI(),
                            "us",
                            "1",
                            available = false
                        )
                    )
                }
        } else {
            val content = Utils.getContext().assets.open("us_numbers.txt").bufferedReader().use {
                it.readText()
            }
            // get random number
            content.split("\n")
                .filter { it.isNotBlank() }
                .shuffled()
                .firstOrNull()
                ?.let {
                    save(
                        TelephonyConfig(
                            number = it,
                            mcc = "310",
                            mnc = "240",
                            iccid = genICCID("310", "1"),
                            "310240" + RandomStringUtils.randomNumeric(9),
                            Utils.generateIMEI(),
                            "us",
                            "1",
                            available = false
                        )
                    )
                }
        }
    }

    @JvmStatic
    fun clear(gsf: Boolean, gms: Boolean, sms: Boolean) {
        try {
            suspend(gsf, gms, sms)
            val cmds: MutableList<String> = ArrayList()
            // suspend
            if (gsf) {
                cmds.add("pm suspend com.google.android.gsf")
                cmds.add("am force-stop com.google.android.gsf")
                cmds.add("echo 'gsf suspended'")
            }
            if (gms) {
                cmds.add("pm suspend com.google.android.gms")
                cmds.add("am force-stop com.google.android.gms")
                cmds.add("echo 'gms suspended'")
            }
            if (sms) {
                cmds.add("pm suspend com.google.android.apps.messaging")
                cmds.add("am force-stop com.google.android.apps.messaging")
                cmds.add("echo 'sms suspended'")
            }
            cmds.add("sleep 1")
            // clear
            if (gsf) {
                cmds.add("pm clear com.google.android.gsf")
                cmds.add("echo 'cleared gsf'")
            }
            if (gms) {
                cmds.add("pm clear com.google.android.gms")
                cmds.add("echo 'cleared gms'")
            }
            if (sms) {
                cmds.add("pm clear com.google.android.apps.messaging")
                cmds.add("echo 'cleared sms'")
            }
            cmds.add("sleep 1")
            // unsuspend
            if (gsf) {
                cmds.add("pm unsuspend com.google.android.gsf")
                cmds.add("echo 'gsf unsuspend'")
            }
            if (gms) {
                cmds.add("pm unsuspend com.google.android.gms")
                cmds.add("echo 'gms unsuspend'")
            }
            if (sms) {
                cmds.add("pm unsuspend com.google.android.apps.messaging")
                cmds.add("echo 'sms unsuspend'")
            }
            cmds.add("sleep 1")
            Utils.runAsRoot(*cmds.toTypedArray<String>())
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

//    @JvmStatic
//    fun stop(gsf: Boolean? = false, gms: Boolean? = false, sms: Boolean? = false) {
//        try {
//            val cmds: MutableList<String> = ArrayList()
//            if (gsf == true) {
//                cmds.add("am force-stop com.google.android.gsf")
//                cmds.add("echo 'stopped gsf'")
//            }
//            if (gms == true) {
//                cmds.add("am force-stop com.google.android.gms")
//                cmds.add("echo 'stopped gms'")
//                Thread.sleep(1000)
//            }
//            if (sms == true) {
//                cmds.add("am force-stop com.google.android.apps.messaging")
//                cmds.add("echo 'stopped sms'")
//            }
//            Utils.runAsRoot(*cmds.toTypedArray<String>())
//        } catch (e: Exception) {
//            e.printStackTrace()
//        }
//    }

    @JvmStatic
    fun suspend(gsf: Boolean? = false, gms: Boolean? = false, sms: Boolean? = false) {
        try {
            val cmds: MutableList<String> = ArrayList()
            if (gsf == true) {
                cmds.add("pm suspend com.google.android.gsf")
                cmds.add("am force-stop com.google.android.gsf")
                cmds.add("echo 'gsf suspended'")
            }
            if (gms == true) {
                cmds.add("pm suspend com.google.android.gms")
                cmds.add("am force-stop com.google.android.gms")
                cmds.add("echo 'gms suspended'")
            }
            if (sms == true) {
                cmds.add("pm suspend com.google.android.apps.messaging")
                cmds.add("am force-stop com.google.android.apps.messaging")
                cmds.add("echo 'sms suspended'")
            }
            cmds.add("sleep 1")
            Utils.runAsRoot(*cmds.toTypedArray<String>())
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    @JvmStatic
    fun unsuspend(gsf: Boolean? = false, gms: Boolean? = false, sms: Boolean? = false) {
        try {
            val cmds: MutableList<String> = ArrayList()
            if (gsf == true) {
                cmds.add("pm unsuspend com.google.android.gsf")
                cmds.add("echo 'gsf unsuspend'")
            }
            if (gms == true) {
                cmds.add("pm unsuspend com.google.android.gms")
                cmds.add("echo 'gms unsuspend'")
            }
            if (sms == true) {
                cmds.add("pm unsuspend com.google.android.apps.messaging")
                cmds.add("echo 'sms unsuspend'")
            }
            cmds.add("sleep 1")
            Utils.runAsRoot(*cmds.toTypedArray<String>())
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    fun sqlite3path(): String {
        val context = Utils.getContext()
        val dataDir = ContextCompat.getDataDir(context)
        val binDir = File(dataDir, "bin")
        val dbDir = File(dataDir, "providerDB")
        if (!binDir.exists()) {
            Utils.copyAssetFolder(context.assets, "bin", binDir.path)
            Utils.copyAssetFolder(context.assets, "providerDB", dbDir.path)
            binDir.listFiles()?.forEach {
                it.setExecutable(true)
            }
        }
        Log.i("Modifier", "arch: " + Build.SUPPORTED_ABIS.joinToString(", "))
        val file = File(dataDir, "bin/sqlite3.arm")
        file.setExecutable(true)
        return file.path
    }

    fun copyDB(): File {
        val context = Utils.getContext()
        val dataDir = ContextCompat.getDataDir(context)
        val dbDir = File(dataDir, "providerDB")
        if (!dbDir.exists()) {
            Utils.copyAssetFolder(context.assets, "providerDB", dbDir.path)
        }
        return dbDir
    }

    @JvmStatic
    suspend fun clearConv() {
        val context = Utils.getContext()
        if (context.getSharedPreferences("settings", Context.MODE_PRIVATE)
                .getBoolean("do_not_clean", false)
        ) {
            return
        }
        val model = Build.MODEL.replace(" ", "_")
        if (!(model == "SM-F707N" || model == "SM-F711B" || model == "Pixel_5")) {
            return
        }
        try {
            val sqlite3 = sqlite3path()
            suspend(sms = true)
            val dataDir = ContextCompat.getDataDir(context)
            val providerDir = File(dataDir, "telephony_provider/$model")
            if (!providerDir.exists()) {
                Utils.copyAssetFolder(context.assets, "telephony_provider/$model", providerDir.path)
            }

            val cmds = mutableListOf<String>()
            fun copyToData(file: File) {
                val relative = file.path.replace(providerDir.path, "")
                if (file.isDirectory) {
                    file.listFiles()?.forEach {
                        copyToData(it)
                    }
                } else {
                    val dest = File("/data/data/com.android.providers.telephony/$relative")
                    cmds.addAll(
                        listOf(
                            "cp -f ${file.path} ${dest.path}",
                            "chmod 660 ${dest.path}",
                            "chown radio:radio ${dest.path}"
                        )
                    )
                }
            }
            copyToData(providerDir)
            shellRun(
                "$sqlite3 /data/data/com.google.android.apps.messaging/databases/bugle_db \"DELETE FROM conversations;\"",
                "$sqlite3 /data/data/com.google.android.apps.messaging/databases/bugle_db \"DELETE FROM messages;\"",
                *cmds.toTypedArray(),
            )
            unsuspend(sms = true)
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    @JvmStatic
    suspend fun resetAll() {
        try {
            clearConv()
            shellRun(
                CMD_SUSPEND_MESSAGING_APP,
                CMD_KILL_MESSAGING_APP,
                CMD_CLEAR_MESSAGING_APP,
                CMD_CLEAR_GSF,
                CMD_CLEAR_GMS,
                "sleep 1",
                CMD_START_PLAY_STORE,
                "sleep 1",
                CMD_HOME,
                "sleep 10",
                CMD_CLEAR_GMS,
                CMD_RESUME_MESSAGING_APP,
                CMD_MESSAGING_APP
            )
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    @JvmStatic
    suspend fun killPhoneProcess(force: Boolean = false): Boolean {
        try {
            if (!force) {
                if (shellRun("getprop phonekilled")["output"]!!.contains("yes")) {
                    return true
                }
            }
            run kill@{
                repeat(3) {
                    val pid = shellRun("pidof com.android.phone")["output"]!!.trim()
                    if (!Regex("[0-9]+").matches(pid)) {
                        Log.e(TAG, "killPhoneProcess: pid not found")
                        return true
                    }
                    Log.i(TAG, "killPhoneProcess: pid=$pid")
                    shellRun("kill -9 $pid")
                    delay(1000)

                    val pidNew = shellRun("pidof com.android.phone")["output"]!!.trim()
                    if (pidNew == pid) {
                        Log.e(TAG, "killPhoneProcess: failed to kill phone process")
                    } else {
                        Log.i(TAG, "killPhoneProcess: success, new pid: $pidNew")
                        shellRun("kill -9 $pid", "setprop phonekilled yes")
                        return true
                    }
                }
            }
        } catch (e: Exception) {
            Log.e(TAG, "Error Kill Phone", e)
        }
        return false
    }

    @JvmStatic
    suspend fun sendSmsFrida(sender: String, msg: String) {
        val context = Utils.getContext()
        try {
            val dataDir = ContextCompat.getDataDir(context)
            Utils.copyAssetFolder(context.assets, "bin", File(dataDir, "bin").path)
            val binPath = File(dataDir, "bin/frida-inject-16.3.3-android-arm64").path
            val pduBase64 =
                String(Base64.getEncoder().encode(RcsHackTool.createFakeSms(sender, msg)))
            Log.i("Modifier", "pduBase64: $pduBase64")
            val script = IOUtils.toString(context.assets.open("scripts/sms.js"), "UTF-8")
                .replace("{pduBase64}", pduBase64)

            val tmpFile: File
            withContext(Dispatchers.IO) {
                tmpFile = File.createTempFile("script", ".js")
                FileUtils.writeStringToFile(tmpFile, script, "UTF-8")
            }

            val pid = Utils.runAsRoot("pidof com.android.phone").trim()
            if (!Regex("[0-9]+").matches(pid)) {
                return
            }

            Log.i("Modifier", "sendSms:  $binPath -p $pid  -s $tmpFile")

            val p = withContext(Dispatchers.IO) {
                Runtime.getRuntime().exec("su -M")
            }
            p.outputStream.bufferedWriter().use {
                it.write("chmod +x $binPath")
                it.newLine()
                it.flush()
                it.write("$binPath -p $pid  -s $tmpFile")
                it.newLine()
                it.flush()
            }
            coroutineScope {
                launch {
                    p.errorStream.bufferedReader().useLines { lines ->
                        lines.forEach {
                            Log.e("Modifier", it)
                        }
                    }
                }
                launch {
                    p.inputStream
                        .bufferedReader()
                        .useLines { lines ->
                            lines.forEach {
                                Log.i("Modifier", it)
                                if (it == "OK") {
                                    p.inputStream.close()
                                    p.errorStream.close()
                                    p.destroy()
                                }
                            }
                        }
                }
            }
            withContext(Dispatchers.IO) {
                p.waitFor()
            }

        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    @JvmStatic
    fun sendSmsIntent(sender: String, msg: String) {
        val intent = Intent()
        intent.setAction("com.example.modifier.sms")
        intent.putExtra("sender", sender)
        intent.putExtra(
            "message",
            msg
        )
        val context = Utils.getContext()
        context.sendBroadcast(intent)
    }

    @JvmStatic
    suspend fun syncTime() {
        try {
            Log.i("Modifier", "syncTime: start")
            val response = KtorClient.head("http://www.baidu.com")
            val dateHeader = response.headers["Date"]
            val date = ZonedDateTime.parse(
                dateHeader,
                DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss z", Locale.ENGLISH)
            )
            // convert to Asia/Shanghai
            val dateInZone = date.withZoneSameInstant(ZoneId.of("Asia/Shanghai"))
            Log.i(
                TAG,
                "CurrentTime from Baidu: ${dateInZone.format(DateTimeFormatter.ISO_DATE_TIME)}"
            )
            shellRun(
                "settings put system time_12_24 24",
                "settings put global auto_time 0",
                "settings put global auto_time_zone 0",
                "setprop persist.sys.timezone Asia/Shanghai",
                "date \"${dateInZone.format(DateTimeFormatter.ofPattern("MMddHHmmyyyy.ss"))}\""
            )
        } catch (e: Exception) {
            Log.e(TAG, "Error SyncTime", e)
        }
    }

    @JvmStatic
    suspend fun hasRoot(): Boolean {
        val hasRoot = run checkRoot@{
            repeat(5) {
                if (Utils.hasRootAccess()) {
                    return@checkRoot true
                }
                delay(500)
            }
            return@checkRoot false
        }
        return hasRoot
    }

    @SuppressLint("DefaultLocale")
    @JvmStatic
    fun genICCID(mnc: String, areaCode: String): String {
        val prefix = String.format("89%02d%s", areaCode.toInt(), mnc)
        return prefix + RandomStringUtils.randomNumeric(20 - prefix.length)
    }

    @JvmStatic
    suspend fun rebooted(): Boolean {
        if (shellRun("getprop rebooted")["output"]!!.contains("yes")) {
            return false
        }
        shellRun("setprop rebooted yes")
        return true
    }

    @SuppressLint("SdCardPath")
    @JvmStatic
    suspend fun backup(
        backupItemDao: BackupItemDao,
        type: String,
        sendCount: Int,
        fresh: Boolean = false
    ): BackupItem {
        clearConv()
        delay(3000)
//        ModifierService.instance!!.toggleRcsSwitch(false)
        val context = Utils.getContext()
        val dest = File(
            ContextCompat.getExternalFilesDirs(context, "backup")[0],
            System.currentTimeMillis().toString()
        )
        dest.mkdirs()
        val file = File(ContextCompat.getDataDir(context), "config.json")
        if (!file.exists()) {
            throw Exception("Config file not found")
        }
        withContext(Dispatchers.IO) {
            IOUtils.copy(
                Files.newInputStream(file.toPath()),
                Files.newOutputStream(File(dest, "config.json").toPath())
            )
        }

        val packages = mutableListOf(
            "com.google.android.apps.messaging",
            "com.google.android.gms",
            "com.google.android.gsf",
        )
        packages.forEach {
            File(dest, it).mkdirs()
        }

        val cmds = mutableListOf<String>()

        for (pkg in packages) {
            if (!shellRun("ls /data/data/$pkg")["error"]!!.contains("No such file or directory")) {
                cmds.add("tar zcpf $dest/$pkg/data.tar.gz -C /data/data $pkg ")
            }
            if (!shellRun("ls /data/user_de/0/$pkg")["error"]!!.contains("No such file or directory")) {
                cmds.add("tar zcpf $dest/$pkg/data_de.tar.gz -C /data/user_de/0 $pkg ")
            }
            if (!shellRun("ls /sdcard/Android/data/$pkg")["error"]!!.contains("No such file or directory")) {
                cmds.add("tar zcpf $dest/$pkg/data_ext.tar.gz -C /sdcard/Android/data $pkg ")
            }
        }

        shellRun(*cmds.toTypedArray())

        val backup = BackupItem(
            createdAt = Date().time,
            number = telephonyConfig.number,
            code = telephonyConfig.areaCode,
            country = telephonyConfig.country,
            mcc = telephonyConfig.mcc,
            mnc = telephonyConfig.mnc,
            imei = telephonyConfig.imei,
            imsi = telephonyConfig.imsi,
            iccid = telephonyConfig.iccid,
            path = dest.path,
            sendCount = sendCount,
            lastUse = Date().time,
            type = type,
            fresh = fresh
        )

        backupItemDao.findBackupForNumber(telephonyConfig.country, telephonyConfig.number)?.let {
            File(it.path).deleteRecursively()
            backupItemDao.delete(it)
            backup.sendCount += it.sendCount
            backup.fresh = false
        }

        backup.id = backupItemDao.insert(backup).toInt()
        return backup
    }

    @SuppressLint("SdCardPath")
    @JvmStatic
    suspend fun restore(backup: BackupItem): Boolean {
        save(
            TelephonyConfig(
                number = backup.number,
                mcc = backup.mcc,
                mnc = backup.mnc,
                country = backup.country,
                areaCode = backup.code,
                iccid = backup.iccid,
                imei = backup.imei,
                imsi = backup.imsi,
                available = true
            ), false
        )
        val packages = mutableListOf(
            "com.google.android.apps.messaging",
            "com.google.android.gms",
            "com.google.android.gsf",
        )

        shellRun(
            "pm disable-user com.google.android.apps.messaging",
            "pm disable-user com.google.android.gms",
            "pm disable-user com.google.android.gsf"
        )
        for (pkg in packages) {
            for (item in listOf("data.tar.gz", "data_de.tar.gz", "data_ext.tar.gz")) {
                val file = File("${backup.path}/$pkg/$item")
                if (!file.exists()) {
                    continue
                }
                val dest = when (item) {
                    "data.tar.gz" -> "/data/data"
                    "data_de.tar.gz" -> "/data/user_de/0"
                    "data_ext.tar.gz" -> "/sdcard/Android/data"
                    else -> ""
                }
                shellRun(
                    "find $dest/$pkg -type f -delete",
                    "tar zxf ${file.path} -C $dest --exclude \"**/AndroidPlatformServices.apk\""
                )
            }
        }
        shellRun(
//            "pm clear com.google.android.gsf",
            "pm enable com.google.android.gsf",
            "sleep 1",
//            "pm clear com.google.android.gms",
            "pm enable com.google.android.gms",
//            "sleep 30",
            "pm enable com.google.android.apps.messaging",
            "sleep 3",
            CMD_MESSAGING_APP,
            "sleep 5",
            "am start -a android.intent.action.SENDTO -d sms:+18583199738 --es sms_body \"Test\" --ez exit_on_sent false"
        )

        return true
    }

    @JvmStatic
    suspend fun optimize() {
        val context = Utils.getContext()
        val packageManager = context.packageManager
        val info = packageManager.getApplicationInfo("com.google.android.gms", 0)

        shellRun(
            "dumpsys deviceidle whitelist +com.google.android.apps.messaging",
            "dumpsys deviceidle whitelist +${BuildConfig.APPLICATION_ID}",
            "cmd netpolicy add restrict-background-blacklist ${info.uid}"
        )
    }

    @JvmStatic
    suspend fun setupSystem() {
        syncTime()
        optimize()
    }

    @JvmStatic
    fun isClashInstalled(context: Context): Boolean {
        val packageManager = context.packageManager
        return try {
            packageManager.getPackageInfo("com.github.metacubex.clash.meta", 0)
            true
        } catch (e: Exception) {
            false
        }
    }
}
