package com.yingmao.clash.act import android.annotation.SuppressLint import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.graphics.Color import android.os.Build import android.os.Bundle import android.text.TextUtils import android.view.View import android.widget.ImageView import android.widget.TextView import androidx.appcompat.widget.AppCompatImageView import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener import com.chad.library.adapter.base.BaseQuickAdapter import com.cncat.vpn.common.constants.Intents import com.cncat.vpn.common.log.Log import com.cncat.vpn.core.Clash import com.cncat.vpn.core.model.TunnelState import com.cncat.vpn.service.remote.IRemoteService import com.google.gson.Gson import com.google.gson.JsonParser import com.tencent.mmkv.MMKV import com.yingmao.clash.BuildConfig import com.yingmao.clash.R import com.yingmao.clash.act.MainActivity.Companion.clashBinder import com.yingmao.clash.act.MainActivity.Companion.profileBinder import com.yingmao.clash.act.MainActivity.Companion.subUrl import com.yingmao.clash.app.MainApplication import com.yingmao.clash.base.BaseActivity import com.yingmao.clash.conf.BZYConstants import com.yingmao.clash.conf.Conf import com.yingmao.clash.entity.ClashNodebBean import com.yingmao.clash.fragment.home.HomeFragment.Companion.CHECK_NODE_REQUEST_CODE import com.yingmao.clash.fragment.home.HomeFragment.Companion.clashRunning import com.yingmao.clash.fragment.home.HomeFragment.Companion.isGlo import com.yingmao.clash.listener.AsyncCallBackListener import com.yingmao.clash.remote.Resource import com.yingmao.clash.util.Utils import com.yingmao.clash.view.RxToast import com.yingmao.clash.view.adapter.NodeListAdapter import com.yingmao.clash.view.dialog.DialogDoMainChoose import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit import okhttp3.Response import java.util.UUID class ChangeNodeActivity : BaseActivity(), View.OnClickListener { companion object { private var currentNode: MutableList = mutableListOf() } private lateinit var swipeLayout: SwipeRefreshLayout private lateinit var changeNodeVM: ChangeNodeVM private lateinit var nodeListRv: RecyclerView private lateinit var aiBack: AppCompatImageView private var broadcastReceiver: BroadcastReceiver? = null private var cacheNodeStr: String? = "" private var uuid: UUID? = null private var testRunDing = false private lateinit var tvProA: TextView private lateinit var tvProj: TextView private var timerJob: Job? = null val remote = Resource() var getNodeTimeOutNum = 5 var isRunDing = false var dialogDoMainChoose: DialogDoMainChoose? = null var proList: MutableList? = null var nowSubUrl: String = "" override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_change_node) changeNodeVM = ViewModelProvider(this)[ChangeNodeVM::class.java] observe(this, changeNodeVM) initData() initViews() } @Deprecated("Deprecated in Java") override fun onBackPressed() { intent.putExtra(BZYConstants.EXTRA_KEY_IS_RUNING, isRunDing) setResult(CHECK_NODE_REQUEST_CODE, intent) super.onBackPressed() } private fun initData() { cacheNodeStr = MMKV.defaultMMKV().decodeString(BZYConstants.NODE_All_ITEM_NAME_KEY, "") val intentFilter = IntentFilter(Intents.ACTION_NODE_LIST) broadcastReceiver = NodeListBR() if (Build.VERSION.SDK_INT >= 34 && applicationInfo.targetSdkVersion >= 34) { registerReceiver( broadcastReceiver, intentFilter, Context.RECEIVER_EXPORTED ) } else { registerReceiver( broadcastReceiver, intentFilter ) } } @SuppressLint("SetTextI18n") private fun initViews() { aiBack = findViewById(R.id.aiBack) tvProA = findViewById(R.id.tvProA) tvProj = findViewById(R.id.tvProj) val isGlobal = MMKV.defaultMMKV().decodeBool(BZYConstants.MMKV_KEY_GLOBAL_MODEL, false) var px: ProxyMode if (isGlobal) { px = ProxyMode.GLOBAL } else { px = ProxyMode.PAC } setBackground(px) tvProA.setOnClickListener { if(!clashRunning){ setBackground(ProxyMode.PAC) MMKV.defaultMMKV().encode(BZYConstants.MMKV_KEY_GLOBAL_MODEL, false) checkGlo() }else{ RxToast.info(resources.getString(R.string.closeRunningChoose)) } if(clashRunning){ RxToast.info(resources.getString(R.string.SwitchingSourceModel)) return@setOnClickListener } } tvProj.setOnClickListener { if(!clashRunning){ MMKV.defaultMMKV().encode(BZYConstants.MMKV_KEY_GLOBAL_MODEL, true) setBackground(ProxyMode.GLOBAL) }else{ RxToast.info(resources.getString(R.string.closeRunningChoose)) } } aiBack.setOnClickListener { onBackPressed() } swipeLayout = findViewById(R.id.swipeLayout) initRefreshLayout() nodeListRv = findViewById(R.id.rv_node_list) findViewById(R.id.iv_back).setOnClickListener { onBackPressed() } initAdapter() if (!TextUtils.isEmpty(cacheNodeStr)) { currentNode = getNodesInfo(cacheNodeStr!!) setNewData(currentNode) } } private fun getNodesInfo(json: String): MutableList { val mList: MutableList = mutableListOf() if (TextUtils.isEmpty(json)) { return mList } val gson = Gson() val ja = JsonParser.parseString(json).asJsonArray val cnb = ClashNodebBean("自动选择", "", "", 1, "", "", 0, true); mList.add(cnb) for (i in 0 until ja.size()) { val bean = gson.fromJson(ja.get(i).asString, ClashNodebBean::class.java) bean.delay = Utils.generateRandomTwoDigitNumber(10, 60) mList.add(bean) } return mList } inner class NodeListBR : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { when (intent?.action) { Intents.ACTION_NODE_LIST -> { Log.d("ACTION_NODE_LIST ACTION_NODE_LIST") hideLoading() timerJob?.cancel() dialogDoMainChoose?.dismiss() val json = serStore.nodes if (TextUtils.isEmpty(json)) { return } //缓存订阅地址 subUrl = nowSubUrl MMKV.defaultMMKV().encode(BZYConstants.PROFILE_SUB_URL, nowSubUrl) //缓存节点 MMKV.defaultMMKV().encode(BZYConstants.NODE_All_ITEM_NAME_KEY, json) currentNode = getNodesInfo(json) setNewData(currentNode) swipeLayout.isRefreshing = false } else -> { } } } } override fun onDestroy() { super.onDestroy() broadcastReceiver?.let { unregisterReceiver(it) } if (dialogDoMainChoose != null) { if (dialogDoMainChoose!!.isShowing) { dialogDoMainChoose!!.dismiss() dialogDoMainChoose = null } } } override fun onResume() { super.onResume() isRunDing = intent.getBooleanExtra(BZYConstants.EXTRA_KEY_IS_RUNING, false) } private fun updateProfile(isDelete: Boolean) { try { launch(Dispatchers.IO) { profileBinder?.let { profileManager -> var mProfile = profileManager.queryAll().find { it.name == Conf.getClashConfName(this@ChangeNodeActivity.applicationContext) } if (mProfile == null) { //为空的情况下选择第一个 profileManager.queryAll().forEach { profile -> mProfile = profile return@forEach } } mProfile?.let { profile -> uuid = profile.uuid Log.d("ChangeNode22 uuid==>${profile.uuid}") if (isDelete) { MMKV.defaultMMKV() .removeValueForKey(BZYConstants.NODE_All_ITEM_NAME_KEY) } else { profileManager.setActive(profile) Log.d("updateProfile pro.source==>${profile.source}, url==>${nowSubUrl}") if (!TextUtils.isEmpty(profile.source)) { if (profile.source != nowSubUrl) { Log.d("updateProfile patch") profileManager.patch( profile.uuid, profile.name, nowSubUrl, 1800000L ) Log.d("updateProfile patch 1") profileManager.commit(profile.uuid) { status -> Log.d("updateProfile updateStatus: " + Gson().toJson(status.args) + ", pro==>" + status.progress) } Log.d("updateProfile patch 2") } } else { profileManager.patch( profile.uuid, profile.name, nowSubUrl, 1800000L ) Log.d("updateProfile patch new 1") profileManager.commit(profile.uuid) { status -> Log.d("updateProfile new updateStatus: " + Gson().toJson(status.args) + ", pro==>" + status.progress) } } Log.d("updateProfile go update") profileManager.update(profile.uuid) } Log.d("gogogogogo end") } } } } catch (e: Exception) { e.printStackTrace() } } private fun initRefreshLayout() { swipeLayout.setColorSchemeColors(Color.rgb(47, 223, 189)) swipeLayout.setOnRefreshListener(object : OnRefreshListener { override fun onRefresh() { if (testRunDing) { RxToast.info("正在测速中."); swipeLayout.isRefreshing = false return } swipeLayout.isRefreshing = true updateProfile(false) swipeLayout.isRefreshing = false changeNodeVM.getAirNodes() } }) } override fun onStart() { super.onStart() subUrl?.let { nowSubUrl = it } doProfile(false) } fun doProfile(isDelete: Boolean) { showLoading("正在获取...", this@ChangeNodeActivity) startTimer() launch(Dispatchers.IO) { MainActivity.setSub2(nowSubUrl) } } private fun changeSubUrl(originalUrl: String, newUrl: String) { if (!originalUrl.isNullOrEmpty() && !newUrl.isNullOrEmpty()) { nowSubUrl = originalUrl.replace(Regex("https://[^/]+/"), "$newUrl/") subUrl = nowSubUrl MMKV.defaultMMKV().encode(BZYConstants.PROFILE_SUB_URL, nowSubUrl) } } val reloadLock = Semaphore(10) private fun setPing() { if (!clashRunning) { RxToast.info("请先开启加速后,再进行测试.") return } if (testRunDing) { RxToast.info("正在测速中.") return } testRunDing = true updateMoreBar("测速中") launch(Dispatchers.IO) { val group = reloadLock.withPermit { clashBinder?.let { cb -> val names = cb.queryProxyGroupNames(false) if (names.isNotEmpty()) { cb.healthCheck(names[0]) } delay(1000) cb.queryProxyGroup(names[0], uiStore.proxySort) } } testRunDing = false runOnUiThread { updateMoreBar("线路测速") } val newData = mutableListOf() group?.proxies?.forEach { proxy -> Log.d("node==>${proxy}") if (currentNode.isNotEmpty()) { for (nodeItem: ClashNodebBean in currentNode) { if (nodeItem.name == proxy.name) { if (proxy.name != "自动选择") { nodeItem.delay = proxy.delay; } Log.d("nodeItem==>${nodeItem}") newData.add(nodeItem) return@forEach } } } } if (newData.isNotEmpty()) { setNewData(newData) } } } private fun setNewData(nodes: MutableList?) { runOnUiThread { if (nodes != null) { nodeListAdapter.setNewInstance(nodes) val selectedName = MMKV.defaultMMKV().decodeString(BZYConstants.NODE_SELECTED_NAME_KEY, "") for ((index, node) in nodes.withIndex()) { if (node.name == selectedName) { nodeListAdapter.selectedPosition = index } } nodeListAdapter.notifyDataSetChanged() } } } var nodeListAdapter: NodeListAdapter = NodeListAdapter(R.layout.node_list_item_layout) private fun initAdapter() { nodeListRv.layoutManager = LinearLayoutManager(this) nodeListRv.adapter = nodeListAdapter nodeListAdapter.selectedPosition nodeListAdapter.setOnItemClickListener { baseQuickAdapter: BaseQuickAdapter<*, *>, view: View, position: Int -> (baseQuickAdapter.getItem(position) as ClashNodebBean).let { profile -> MMKV.defaultMMKV().encode(BZYConstants.NODE_SELECTED_NAME_KEY, profile.name) nodeListAdapter.notifyItemChanged(nodeListAdapter.selectedPosition) nodeListAdapter.selectedPosition = if (nodeListAdapter.selectedPosition == position) -1 else position nodeListAdapter.notifyItemChanged(position) launch(Dispatchers.IO) { val intent = Intent() intent.putExtra(BZYConstants.MMKV_KEY_SELECT_NODE_NAME, profile.name) intent.putExtra(BZYConstants.EXTRA_KEY_IS_RUNING, isRunDing) setResult(CHECK_NODE_REQUEST_CODE, intent) finish() } } } } private fun startTimer() { getNodeTimeOutNum = 15 timerJob = launch { repeat(16) { Log.d("GetNodeTimeOutNum==>$getNodeTimeOutNum") if (getNodeTimeOutNum == 0) { hideLoading() Log.d("stopTimer...") RxToast.error("获取节点失败...正在切换其他地址") if (setNewData()) { timerJob?.cancel() MMKV.defaultMMKV() .removeValueForKey(BZYConstants.NODE_All_ITEM_NAME_KEY) saveSubUrl() MainApplication.restartApp(this@ChangeNodeActivity, object : MainApplication.OnRestartButtonListener { override fun enterButton() { } override fun cancelButton() { // doProfile(false) } }) } else { timerJob?.cancel() RxToast.error("获取全部线路失败...请切换网络重试") } } getNodeTimeOutNum-- delay(1000) } } } override fun onClick(p0: View?) { when (p0?.id) { R.id.tv_more -> { setPing() } else -> { } } } private fun setBackground(proxyMode: ProxyMode) { when (proxyMode) { ProxyMode.PAC -> { isGlo = false tvProA.setBackgroundResource(R.drawable.yil_node_proxy_choose_bg) tvProj.setBackgroundResource(0) MMKV.defaultMMKV().encode(BZYConstants.MMKV_KEY_GLOBAL_MODEL, false) sendMode(false) } ProxyMode.GLOBAL -> { isGlo = true tvProA.setBackgroundResource(0) tvProj.setBackgroundResource(R.drawable.yil_node_proxy_choose_bg) MMKV.defaultMMKV().encode(BZYConstants.MMKV_KEY_GLOBAL_MODEL, true) sendMode(true) } } } fun sendMode(type: Boolean) { val intent = Intent(Intents.ACTION_Glo_Mode) intent.putExtra("mode", type) sendBroadcast( intent.setPackage(this.packageName) ) } private fun setNewData(): Boolean { val mmkv = MMKV.defaultMMKV() val originalUrl = mmkv.decodeString(BZYConstants.MMKV_KEY_CURRENT_HOST, BuildConfig.service_url) var newUrlString = originalUrl var isChange = false val routerHost = mmkv.decodeStringSet(BZYConstants.MMKV_KEY_ROUTER_HOST, HashSet()) val switchFinishSet = mmkv.decodeStringSet(BZYConstants.MMKV_KEY_Switch_Finsh_Set, HashSet()) routerHost?.let { if (it.isEmpty()) { android.util.Log.d(Conf.TAG, "routerHost isEmpty") return false } Log.d("switchFinshSet.size==>${switchFinishSet?.size!!} , routerHost.size==>${routerHost.size}") //次数已满 if (switchFinishSet.size >= routerHost.size) { return false } android.util.Log.d(Conf.TAG, "routerHost not empty") for (url in it) { if (url == originalUrl) { switchFinishSet.add(url) mmkv.encode(BZYConstants.MMKV_KEY_Switch_Finsh_Set, switchFinishSet) continue } if (switchFinishSet.contains(url)) { continue } else { isChange = true android.util.Log.e("Retry", "切换到新url:$url") newUrlString = url switchFinishSet.add(newUrlString) mmkv.encode(BZYConstants.MMKV_KEY_Switch_Finsh_Set, switchFinishSet) break } } mmkv.encode(BZYConstants.MMKV_KEY_CURRENT_HOST, newUrlString) } return isChange } private fun saveSubUrl() { val currentHost = MMKV.defaultMMKV() .decodeString(BZYConstants.MMKV_KEY_CURRENT_HOST, "") Log.d("tryButton currentHost==>${currentHost}") val oldUrl = nowSubUrl currentHost?.let { changeSubUrl(oldUrl, it) } } enum class ProxyMode { PAC, GLOBAL } private fun checkGlo() { val isGlobal = isGlo val isGlobal2 = MMKV.defaultMMKV().decodeBool(BZYConstants.MMKV_KEY_GLOBAL_MODEL, false) Log.i("checkGlo isGlobal==>${isGlobal}, isGlobal2==>${isGlobal2}") val px: ProxyMode = if (isGlobal2) { ProxyMode.GLOBAL } else { ProxyMode.PAC } setBackground(px) launch(Dispatchers.IO) { if (isGlobal) { if (clashRunning) { getNodes() } } else { if (clashRunning) { delay(1000) clashBinder?.let { cm -> val names = cm.queryProxyGroupNames(false) Log.i("checkGlo names==>${names}") if (names.isNotEmpty()) { val selectedName = MMKV.defaultMMKV() .decodeString(BZYConstants.NODE_SELECTED_NAME_KEY, ""); Log.i("selected_name ==>${selectedName}") if (!TextUtils.isEmpty(selectedName)) { val o = cm.queryOverride(Clash.OverrideSlot.Session) o.mode = TunnelState.Mode.Rule cm.patchOverride(Clash.OverrideSlot.Session, o) delay(500) var gName = names[0] //全局模式下 names[0]会自动添加一个 "GLOBAL" 组 if (names.size >= 2) { if ("GLOBAL" == gName) { //names[0] 为 "GLOBAL" //names[1]才是规则模式下的组名,所以需要手动赋值 gName = names[1] } } cm.patchSelector(gName, selectedName!!) } } } } } } } private suspend fun getNodes() { coroutineScope { val selectedName = MMKV.defaultMMKV().decodeString(BZYConstants.NODE_SELECTED_NAME_KEY, "") Log.d("getNodes selected_name==>${selectedName}") if (TextUtils.isEmpty(selectedName)) { subUrl?.let { changeNodeVM.getNodeOther(it, object : AsyncCallBackListener { override fun suc(obj: Any?) { proList = obj as MutableList proList?.let { proListItem -> val nodeName = proListItem[0].name if (!TextUtils.isEmpty(nodeName)) { launch(Dispatchers.IO) { clashBinder?.let { cm -> val o = cm.queryOverride(Clash.OverrideSlot.Session) o.mode = TunnelState.Mode.Global cm.patchOverride(Clash.OverrideSlot.Session, o) delay(3000) //等待全局模式生效 var maxIndex = 0 while (true) { if (maxIndex > 3) { break } val names = cm.queryProxyGroupNames(false) if (names.isNotEmpty()) { if (names[0] == "GLOBAL") { Log.d("repeat end") break } } Log.d("repeat agent") maxIndex++ delay(1000) } cm.patchSelector("GLOBAL", nodeName) } } } } } override fun faild(response: Response) { } override fun err(msg: String, exception: Exception?) { } }) } } else { clashBinder?.let { cm -> val o = cm.queryOverride(Clash.OverrideSlot.Session) o.mode = TunnelState.Mode.Global cm.patchOverride(Clash.OverrideSlot.Session, o) delay(3000) //等待全局 var maxIndex = 0 while (true) { if (maxIndex > 3) { break } val names = cm.queryProxyGroupNames(false) if (names.isNotEmpty()) { if (names[0] == "GLOBAL") { Log.d("repeat end") break } } Log.d("repeat agent") maxIndex++ delay(1000) } cm.patchSelector("GLOBAL", selectedName!!) } } } } }