598 lines
22 KiB
Kotlin
598 lines
22 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.service.model.Profile
|
|
import com.cncat.vpn.service.remote.IProfileManager
|
|
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.R
|
|
import com.yingmao.clash.act.ProxyModeActivity.ProxyMode
|
|
import com.yingmao.clash.act.NewMainActivity.Companion.clashBinder
|
|
import com.yingmao.clash.act.NewMainActivity.Companion.profileBinder
|
|
import com.yingmao.clash.act.NewMainActivity.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.NewAccFragment.Companion.clashRunning
|
|
import com.yingmao.clash.fragment.NewAccFragment.Companion.isGlo
|
|
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.MainScope
|
|
import kotlinx.coroutines.delay
|
|
import kotlinx.coroutines.launch
|
|
import kotlinx.coroutines.sync.Semaphore
|
|
import kotlinx.coroutines.sync.withPermit
|
|
import java.util.Timer
|
|
import java.util.TimerTask
|
|
import java.util.UUID
|
|
|
|
class ChangeNodeActivity : BaseActivity() {
|
|
|
|
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 testRuning = false
|
|
private lateinit var tvProA:TextView
|
|
private lateinit var tvProj:TextView
|
|
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
super.onCreate(savedInstanceState)
|
|
setContentView(R.layout.activity_change_node)
|
|
changeNodeVM = ViewModelProvider(this)[ChangeNodeVM::class.java]
|
|
initViews()
|
|
initData()
|
|
}
|
|
|
|
override fun onBackPressed() {
|
|
intent.putExtra(BZYConstants.EXTRA_KEY_IS_RUNING, is_runing)
|
|
setResult(2, intent)
|
|
super.onBackPressed()
|
|
}
|
|
|
|
private fun initData() {
|
|
val isGlobal = MMKV.defaultMMKV().decodeBool(BZYConstants.MMKV_KEY_GLOBAL_MODEL, false)
|
|
val px: ProxyMode
|
|
if (isGlobal) {
|
|
px = ProxyMode.GLOBAL
|
|
} else {
|
|
px = ProxyMode.PAC
|
|
}
|
|
setBackground(px)
|
|
cacheNodeStr = MMKV.defaultMMKV().decodeString(BZYConstants.NODE_All_ITEM_NAME_KEY, "")
|
|
|
|
val intentFilter = IntentFilter(Intents.ACTION_NODE_LIST)
|
|
android.util.Log.i("NODETA","DAAAAqqqqq::AA")
|
|
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() {
|
|
tvProA=findViewById(R.id.tvProA)
|
|
tvProj=findViewById(R.id.tvProj)
|
|
|
|
aiBack = findViewById(R.id.aiBack)
|
|
swipeLayout = findViewById(R.id.swipeLayout)
|
|
nodeListRv = findViewById(R.id.rv_node_list)
|
|
tvProA.setOnClickListener {
|
|
setBackground(ProxyMode.PAC)
|
|
MMKV.defaultMMKV().encode(BZYConstants.MMKV_KEY_GLOBAL_MODEL, false)
|
|
|
|
}
|
|
tvProj.setOnClickListener {
|
|
MMKV.defaultMMKV().encode(BZYConstants.MMKV_KEY_GLOBAL_MODEL, true)
|
|
setBackground(ProxyMode.GLOBAL)
|
|
}
|
|
|
|
aiBack.setOnClickListener {
|
|
onBackPressed()
|
|
}
|
|
initRefreshLayout()
|
|
var userTrafficTv: TextView = findViewById(R.id.node_user_traffic)
|
|
|
|
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()) {
|
|
var 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?) {
|
|
android.util.Log.i("NODETA","DAAAA::"+intent?.action)
|
|
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
|
|
}
|
|
android.util.Log.i("NODETA","subUrl::"+subUrl)
|
|
android.util.Log.i("NODETA","json::"+json)
|
|
//缓存订阅地址
|
|
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)
|
|
}
|
|
}
|
|
|
|
override fun onResume() {
|
|
super.onResume()
|
|
// uuid = intent.getStringExtra(BZYConstants.MMKV_KEY_CURRENT_NODE)
|
|
// com.cncat.vpn.common.log.Log.d("ChangeNode uuid==>$uuid")
|
|
is_runing = intent.getBooleanExtra(BZYConstants.EXTRA_KEY_IS_RUNING, false)
|
|
}
|
|
|
|
val remote = Resource<IRemoteService>()
|
|
var GetNodeTimeOutNum = 5
|
|
var is_runing = false
|
|
private var timer: Timer? = null
|
|
private var timerTask: TimerTask? = null
|
|
var nowSubUrl: String = ""
|
|
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) {
|
|
Log.d("gogogogogo isDelete")
|
|
profileManager.delete(profile.uuid)
|
|
MMKV.defaultMMKV()
|
|
.removeValueForKey(BZYConstants.NODE_All_ITEM_NAME_KEY)
|
|
setSub2(profileManager, nowSubUrl)
|
|
} else {
|
|
Log.d("gogogogogo update")
|
|
profileManager.setActive(profile)
|
|
delay(500)
|
|
if (!TextUtils.isEmpty(profile.source)) {
|
|
// Log.d("updateProfile pro.source==>${profile.source}, url==>${nowSubUrl}")
|
|
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")
|
|
}
|
|
}
|
|
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 (testRuning) {
|
|
RxToast.info("正在测速中.");
|
|
swipeLayout.isRefreshing = false
|
|
return
|
|
}
|
|
com.cncat.vpn.common.log.Log.d("onRefresh")
|
|
// getNodesStatus();
|
|
// changeNodeVM.getAirNodes()
|
|
swipeLayout.isRefreshing = true
|
|
updateProfile(false)
|
|
com.cncat.vpn.common.log.Log.d("onRefresh end")
|
|
swipeLayout.isRefreshing = false
|
|
}
|
|
})
|
|
}
|
|
|
|
override fun onStart() {
|
|
super.onStart()
|
|
subUrl?.let {
|
|
nowSubUrl = it
|
|
}
|
|
doProfile()
|
|
|
|
}
|
|
|
|
fun doProfile() {
|
|
showLoading("正在获取...", this@ChangeNodeActivity)
|
|
startTimer()
|
|
updateProfile(false)
|
|
|
|
}
|
|
|
|
private fun changeSubUrl(originalUrl: String, newUrl: String) {
|
|
if (!originalUrl.isNullOrEmpty() && !newUrl.isNullOrEmpty()) {
|
|
nowSubUrl = originalUrl.replace(Regex("https://[^/]+/"), "$newUrl/")
|
|
MMKV.defaultMMKV().encode(BZYConstants.PROFILE_SUB_URL, nowSubUrl)
|
|
}
|
|
}
|
|
|
|
|
|
val reloadLock = Semaphore(10)
|
|
private fun setPing() {
|
|
if (!clashRunning) {
|
|
RxToast.info("请先开启加速后,再进行测试.")
|
|
return
|
|
}
|
|
if (testRuning) {
|
|
RxToast.info("正在测速中.")
|
|
return
|
|
}
|
|
testRuning = true
|
|
launch(Dispatchers.IO) {
|
|
var group = reloadLock.withPermit {
|
|
clashBinder?.let { cb ->
|
|
var names = cb.queryProxyGroupNames(false)
|
|
if (names.isNotEmpty()) {
|
|
cb.healthCheck(names[0])
|
|
delay(1000)
|
|
cb.queryProxyGroup(names[0], uiStore.proxySort);
|
|
} else {
|
|
null
|
|
}
|
|
}
|
|
}
|
|
testRuning = false
|
|
var newDatas = mutableListOf<ClashNodebBean>();
|
|
group?.proxies?.forEach { proxy ->
|
|
Log.d("node==>${proxy}")
|
|
if (currentNode?.isNotEmpty() == true) {
|
|
for (nodeItem: ClashNodebBean in currentNode!!) {
|
|
if (nodeItem.name == proxy.name) {
|
|
if (proxy.name != "自动选择") {
|
|
nodeItem.delay = proxy.delay;
|
|
}
|
|
Log.d("nodeItem==>${nodeItem}")
|
|
newDatas.add(nodeItem)
|
|
return@forEach
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (newDatas.isNotEmpty()) {
|
|
setNewData(newDatas)
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
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 ->
|
|
Log.i("select_node : ${profile.name}")
|
|
// checkedNode = 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)
|
|
android.util.Log.i("AAAAAAAAAAAAAA:","profile.name:::"+{profile.name})
|
|
launch(Dispatchers.IO) {
|
|
val intent = Intent()
|
|
intent.putExtra(BZYConstants.MMKV_KEY_SELECT_NODE_NAME, profile.name)
|
|
intent.putExtra(BZYConstants.EXTRA_KEY_IS_RUNING, is_runing)
|
|
setResult(2, intent)
|
|
finish()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun createProfile() {
|
|
launch(Dispatchers.IO) {
|
|
profileBinder?.let { pro ->
|
|
uuid?.let { uu ->
|
|
pro.delete(uu)
|
|
delay(500)
|
|
setSub2(pro, subUrl)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public suspend fun setSub2(pm: IProfileManager, url: String?) {
|
|
// com.cncat.vpn.common.log.Log.d("setSub2.... url==>$url")
|
|
if (TextUtils.isEmpty(url)) {
|
|
com.cncat.vpn.common.log.Log.d("setSub2 url isEmpty")
|
|
return
|
|
}
|
|
|
|
com.cncat.vpn.common.log.Log.d("setSub2 2")
|
|
|
|
|
|
com.cncat.vpn.common.log.Log.d("setSub2 3")
|
|
val pro = pm.queryAll().find {
|
|
it.name == Conf.getClashConfName(this@ChangeNodeActivity.applicationContext)
|
|
}
|
|
com.cncat.vpn.common.log.Log.d("setSub2 4")
|
|
pro?.let {
|
|
com.cncat.vpn.common.log.Log.d("setSub2 pro 已存在 ==>")
|
|
// pm.setActive(it)
|
|
|
|
com.cncat.vpn.common.log.Log.d("setSub2 setActive end")
|
|
} ?: run {
|
|
var uuid: UUID = pm.create(
|
|
Profile.Type.Url,
|
|
Conf.getClashConfName(this@ChangeNodeActivity.applicationContext)
|
|
)
|
|
delay(500)
|
|
pm.queryByUUID(uuid)?.let { qPro ->
|
|
pm.setActive(qPro)
|
|
pm.patch(
|
|
qPro.uuid, qPro.name,
|
|
// "https://mojie0201.xn--8stx8olrwkucjq3b.com/api/v1/client/subscribe?token=169110d2674c49604b7e5b5cea341579",
|
|
// "https://www.otcopusapp.cc/lx3af288h5i8pz380/api/v1/client/subscribe?token=d6bc98c7ea53099fa5bd1a3ea415ebb6",
|
|
url!!,
|
|
1800000L
|
|
)
|
|
// coroutineScope {
|
|
pm.commit(qPro.uuid) { status ->
|
|
android.util.Log.d(
|
|
Conf.TAG,
|
|
"new updateStatus: " + Gson().toJson(status.args) + ", pro==>" + status.progress
|
|
)
|
|
}
|
|
// }
|
|
}
|
|
}
|
|
}
|
|
|
|
var isCreate: Boolean = false
|
|
private var timerJob: Job? = null
|
|
private fun startTimer() {
|
|
GetNodeTimeOutNum = 15
|
|
timerJob = launch {
|
|
repeat(16) {
|
|
Log.d("GetNodeTimeOutNum==>$GetNodeTimeOutNum")
|
|
if (GetNodeTimeOutNum == 0) {
|
|
hideLoading()
|
|
Log.d("stopTimer...")
|
|
RxToast.error("获取节点失败...请尝试切换其他地址")
|
|
showDomainList()
|
|
timerJob?.cancel()
|
|
// delay(1000)
|
|
// this@ChangeNodeActivity.finish()
|
|
|
|
}
|
|
GetNodeTimeOutNum--
|
|
delay(1000)
|
|
}
|
|
}
|
|
}
|
|
|
|
var dialogDoMainChoose: DialogDoMainChoose? = null
|
|
private fun showDomainList() {
|
|
try {
|
|
SplashHelper(changeNodeVM).getHostWithFallback { success ->
|
|
MainScope().launch {
|
|
|
|
if (success) {
|
|
if (dialogDoMainChoose == null) {
|
|
dialogDoMainChoose = DialogDoMainChoose(this@ChangeNodeActivity)
|
|
}
|
|
if (dialogDoMainChoose?.isShowing == false) {
|
|
dialogDoMainChoose?.setOnTryButtonListener(object :
|
|
DialogDoMainChoose.OnTryButtonListener {
|
|
override fun tryButton() {
|
|
saveSubUrl()
|
|
doProfile()
|
|
dialogDoMainChoose?.dismiss()
|
|
}
|
|
|
|
override fun cancel() {
|
|
com.cncat.vpn.common.log.Log.d("tryButton CHECK_LOGIN cancel")
|
|
|
|
dialogDoMainChoose?.dismiss()
|
|
}
|
|
})
|
|
dialogDoMainChoose?.setOnListItemListener(object :
|
|
DialogDoMainChoose.OnListItemListener {
|
|
override fun listItemClick() {
|
|
saveSubUrl()
|
|
MainApplication.restartApp(this@ChangeNodeActivity, object :
|
|
MainApplication.OnRestartButtonListener {
|
|
|
|
override fun enterButton() {
|
|
|
|
}
|
|
|
|
override fun cancelButton() {
|
|
doProfile()
|
|
dialogDoMainChoose?.dismiss()
|
|
}
|
|
|
|
})
|
|
}
|
|
})
|
|
dialogDoMainChoose?.show()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
} catch (e: Exception) {
|
|
e.printStackTrace()
|
|
}
|
|
}
|
|
|
|
private fun saveSubUrl() {
|
|
var currentHost = MMKV.defaultMMKV()
|
|
.decodeString(BZYConstants.MMKV_KEY_CURRENT_HOST, "")
|
|
var oldUrl = nowSubUrl
|
|
currentHost?.let { changeSubUrl(oldUrl, it) }
|
|
}
|
|
|
|
private fun stopTimer() {
|
|
timerTask?.cancel()
|
|
timer?.purge()
|
|
timer?.cancel()
|
|
timerTask = null
|
|
timer = null
|
|
}
|
|
private fun setBackground(proxyMode: ProxyMode) {
|
|
when (proxyMode) {
|
|
ProxyMode.PAC -> {
|
|
isGlo = false
|
|
tvProA.setBackgroundResource(R.drawable.oc_proxy_choose_bg)
|
|
tvProj.setBackgroundResource(0)
|
|
tvProA.setTextColor(Color.parseColor("#FFFFFF"))
|
|
tvProj.setTextColor(Color.parseColor("#B36C1B"))
|
|
sendMode(false)
|
|
}
|
|
|
|
ProxyMode.GLOBAL -> {
|
|
isGlo = true
|
|
tvProA.setBackgroundResource(0)
|
|
tvProA.setTextColor(Color.parseColor("#B36C1B"))
|
|
tvProj.setTextColor(Color.parseColor("#FFFFFF"))
|
|
tvProj.setBackgroundResource(R.drawable.oc_proxy_choose_bg)
|
|
sendMode(true)
|
|
}
|
|
else->{
|
|
|
|
}
|
|
}
|
|
}
|
|
fun sendMode(type: Boolean) {
|
|
val intent = Intent(Intents.ACTION_Glo_Mode)
|
|
intent.putExtra("mode", type)
|
|
sendBroadcast(
|
|
intent.setPackage(this.packageName)
|
|
)
|
|
}
|
|
}
|