yijianlianUI/app/src/main/java/com/yingmao/clash/act/ChangeNodeActivity.kt

703 lines
26 KiB
Kotlin

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<ClashNodebBean> = 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<IRemoteService>()
var getNodeTimeOutNum = 5
var isRunDing = false
var dialogDoMainChoose: DialogDoMainChoose? = null
var proList: MutableList<ClashNodebBean>? = 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<ImageView>(R.id.iv_back).setOnClickListener {
onBackPressed()
}
initAdapter()
if (!TextUtils.isEmpty(cacheNodeStr)) {
currentNode = getNodesInfo(cacheNodeStr!!)
setNewData(currentNode)
}
}
private fun getNodesInfo(json: String): MutableList<ClashNodebBean> {
val mList: MutableList<ClashNodebBean> = 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<ClashNodebBean>()
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<ClashNodebBean>?) {
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<ClashNodebBean>
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!!)
}
}
}
}
}