diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index bd3a3a1..8e87f9f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -28,6 +28,9 @@
+
+
+
+ >
@@ -143,6 +146,15 @@
android:name=".act.NodeGroupActivity"
android:process=":background"
android:screenOrientation="portrait" />
+
+
+
()
private lateinit var mainAtyVM: MainAtyVM
private var payResultLoading: RxDialogShapeLoading? = null
private val clashRunning: Boolean
@@ -161,6 +162,7 @@ class MainActivity : BaseActivity() {
pm.update(it.uuid)
newUuid = pro.uuid.toString()
isSubbed = true
+ AppLog.i("MainActivity", "刷新机场:${newUuid}")
} ?: run {
val uuid: UUID = pm.create(
Profile.Type.Url,
@@ -216,6 +218,23 @@ class MainActivity : BaseActivity() {
startLog()
firstEntry()
mainAtyVM.getExpensesRecord(mPageInfo)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ val requestPermissionLauncher =
+ registerForActivityResult(
+ ActivityResultContracts.RequestPermission()
+ ) { isGranted: Boolean ->
+ }
+ if (ContextCompat.checkSelfPermission(
+ this,
+ android.Manifest.permission.POST_NOTIFICATIONS
+ ) != PackageManager.PERMISSION_GRANTED
+ ) {
+ requestPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS)
+ }
+ }
+ //初始进入 将日志采集开关变为不开启
+ MMKV.defaultMMKV().encode(BZYConstants.BehaviorLogSwitch, false)
}
inner class NodeListBR : BroadcastReceiver() {
@@ -266,6 +285,7 @@ class MainActivity : BaseActivity() {
aivHome.setImageResource(R.drawable.yjl_home_page_choose)
aivCenter.setImageResource(R.drawable.yjl_user_page)
aivVip.setImageResource(R.drawable.yjl_vip_page)
+ checkVPN()
}
aivCenter.setOnClickListener {
@@ -274,6 +294,7 @@ class MainActivity : BaseActivity() {
aivHome.setImageResource(R.drawable.yjl_home_page)
aivCenter.setImageResource(R.drawable.yjl_user_page_choose)
aivVip.setImageResource(R.drawable.yjl_vip_page)
+ checkVPN()
}
aivVip.setOnClickListener {
@@ -334,6 +355,7 @@ class MainActivity : BaseActivity() {
lifecycleScope.launch(Dispatchers.IO) {
subUrl?.let {
Log.i("netSubUrl:${netSubUrl}::保存机场URL:=${proSubUrl}::subUrl:${subUrl}")
+ AppLog.i("MainActivity", "网络获取机场netSubUrl:${netSubUrl}")
setSub2(it)
}
}
@@ -376,7 +398,7 @@ class MainActivity : BaseActivity() {
val oldExpireTime =
mmkv.decodeLong(BZYConstants.MMKV_KEY_VPN_VALID_PERIOD, 0)
val nowExpireTime = checkLoginRsp.data!!.vpnExpireTime
-
+ AppLog.i("MainActivity", "刷新有效期:")
if (nowExpireTime!=oldExpireTime) {
val expired = checkLoginRsp.data.expired
mmkv.encode(BZYConstants.MMKV_KEY_VPN_EXPIRED, expired ?: true)
@@ -397,8 +419,6 @@ class MainActivity : BaseActivity() {
}else if(checkLoginRsp.code == -2){
RxToast.info(resources.getString(R.string.LoginOut))
cleanUserInfo()
- }else {
- RxToast.info(resources.getString(R.string.QueryFailed))
}
payResultLoading?.dismiss()
}
@@ -505,7 +525,7 @@ class MainActivity : BaseActivity() {
HttpReqType.CHECK_LOGIN -> {
payResultLoading?.dismiss()
- RxToast.error(resources.getString(R.string.QueryFailed))
+ RxToast.error(resources.getString(R.string.QueryFailed)+ httpStatus.msg)
}
HttpReqType.GET_AIRNODES -> {
@@ -529,6 +549,7 @@ class MainActivity : BaseActivity() {
}
fun cleanUserInfo() {
+ AppLog.i("MainActivity", "cleanUserInfo===>退出登录")
MMKV.defaultMMKV().removeValueForKey(BZYConstants.MMKV_KEY_PH_TOKEN)
MMKV.defaultMMKV().removeValueForKey(BZYConstants.MMKV_KEY_TOKEN)
MMKV.defaultMMKV().removeValueForKey(BZYConstants.MMKV_KEY_CURRENT_NODE)
@@ -783,4 +804,17 @@ class MainActivity : BaseActivity() {
aivCenter.setImageResource(R.drawable.yjl_user_page)
aivVip.setImageResource(R.drawable.yjl_vip_page_choose)
}
+ fun checkVPN(){
+ val behaviorLogSwitch =
+ MMKV.defaultMMKV().decodeBool(BZYConstants.BehaviorLogSwitch, false)
+ if(behaviorLogSwitch){
+ launch(Dispatchers.IO) {
+ val isVPN =Utils.isVpnActive(this@MainActivity)
+ AppLog.i("NewMainActivity", "BroadcastReceiver::isVPN::${isVPN}")
+ val vpnCheChek =Utils.checkHttpOk("https://www.youtube.com")
+ AppLog.i("NewMainActivity", "BroadcastReceiver::vpnCheChek::${vpnCheChek}")
+ }
+ }
+
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/yingmao/clash/act/SettingActivity.kt b/app/src/main/java/com/yingmao/clash/act/SettingActivity.kt
index b65638f..c6bed99 100644
--- a/app/src/main/java/com/yingmao/clash/act/SettingActivity.kt
+++ b/app/src/main/java/com/yingmao/clash/act/SettingActivity.kt
@@ -44,6 +44,7 @@ import com.yingmao.clash.conf.BZYConstants
import com.yingmao.clash.conf.Conf
import com.yingmao.clash.entity.LanguageInfoEntity
import com.yingmao.clash.fragment.home.HomeFragment.Companion.clashRunning
+import com.yingmao.clash.levellog.AppLog
import com.yingmao.clash.net.http.HttpReqType
import com.yingmao.clash.net.http.HttpStatus
import com.yingmao.clash.net.http.entity.AppVersionCheckRsp
@@ -195,6 +196,7 @@ class SettingActivity : BaseActivity() {
}
private fun cleanUserInfo() {
+ AppLog.i("SettingActivity", "退出登录 cleanUserInfo clashRunning==>${clashRunning}")
MMKV.defaultMMKV().removeValueForKey(BZYConstants.MMKV_KEY_PH_TOKEN)
MMKV.defaultMMKV().removeValueForKey(BZYConstants.MMKV_KEY_TOKEN)
MMKV.defaultMMKV().removeValueForKey(BZYConstants.MMKV_KEY_CURRENT_NODE)
diff --git a/app/src/main/java/com/yingmao/clash/act/SplashActivity.kt b/app/src/main/java/com/yingmao/clash/act/SplashActivity.kt
index 44c0c8d..cee7254 100644
--- a/app/src/main/java/com/yingmao/clash/act/SplashActivity.kt
+++ b/app/src/main/java/com/yingmao/clash/act/SplashActivity.kt
@@ -18,6 +18,8 @@ import android.widget.ProgressBar
import android.widget.RelativeLayout
import android.widget.TextView
import androidx.core.content.FileProvider
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import com.afollestad.materialdialogs.MaterialDialog
@@ -92,6 +94,12 @@ class SplashActivity : BaseActivity() {
splashAtyVM = ViewModelProvider(this).get(SplashAtyVM::class.java)
observe(this, splashAtyVM)
setContentView(R.layout.layout_splash_activity)
+
+ ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.rlMain)) { v, insets ->
+ val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
+ v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
+ insets
+ }
rlMain = findViewById(R.id.rlMain)
rlPb = findViewById(R.id.rl_pb)
llAction = findViewById(R.id.ll_action)
diff --git a/app/src/main/java/com/yingmao/clash/app/MainApplication.kt b/app/src/main/java/com/yingmao/clash/app/MainApplication.kt
index 6c20662..b1fed51 100644
--- a/app/src/main/java/com/yingmao/clash/app/MainApplication.kt
+++ b/app/src/main/java/com/yingmao/clash/app/MainApplication.kt
@@ -23,6 +23,10 @@ import com.tencent.mmkv.MMKV
import com.yingmao.clash.R
import com.yingmao.clash.conf.Constants
import com.yingmao.clash.entity.UserData
+import com.yingmao.clash.levellog.LogDispatcher
+import com.yingmao.clash.levellog.LogEvent
+import com.yingmao.clash.levellog.LogFileWriter
+import com.yingmao.clash.levellog.LogLevel
import com.yingmao.clash.util.CrashCatchHandler
import com.yingmao.clash.util.clashDir
import com.yingmao.clash.view.dialog.DialogSureRestartChoose
@@ -138,6 +142,24 @@ class MainApplication : Application() {
BaiduAction.init(this, Constants.USER_ACTION_SET_ID, Constants.APP_SECRET_KEY)
// 设置应用激活的间隔,此方法在初始化成功后才有意义,建议在初始化后执行,具体执行的时机没有限制
BaiduAction.setActivateInterval(this, 7)
+
+ LogFileWriter.init(this)
+
+ // 可选:捕获全局崩溃
+ Thread.setDefaultUncaughtExceptionHandler { _, throwable ->
+ LogDispatcher.enqueue(
+ LogEvent(
+ time = System.currentTimeMillis(),
+ level = LogLevel.ERROR,
+ tag = "Crash",
+ message = "Uncaught exception",
+ throwable = throwable
+ )
+ )
+
+ // 给日志落盘时间
+ try { Thread.sleep(200) } catch (_: Exception) {}
+ }
}
private fun extractGeoFiles() {
clashDir.mkdirs()
diff --git a/app/src/main/java/com/yingmao/clash/base/BaseActivity.kt b/app/src/main/java/com/yingmao/clash/base/BaseActivity.kt
index 90f6972..8fa4b59 100644
--- a/app/src/main/java/com/yingmao/clash/base/BaseActivity.kt
+++ b/app/src/main/java/com/yingmao/clash/base/BaseActivity.kt
@@ -18,14 +18,10 @@ import com.yingmao.clash.BuildConfig
import com.yingmao.clash.R
import com.yingmao.clash.conf.BZYConstants
import com.yingmao.clash.conf.Conf
-//import com.yingmao.clash.design.model.DarkMode
-//import com.yingmao.clash.design.store.UiStore
-//import com.yingmao.clash.design.ui.DayNight
import com.yingmao.clash.net.http.HttpStatus
import com.yingmao.clash.remote.Broadcasts
import com.yingmao.clash.remote.Remote
import com.cncat.vpn.service.store.ServiceStore
-import com.yingmao.clash.conf.BZYConstants.Companion.ScreenFlag
import com.yingmao.clash.util.StatusBarUtils
import com.yingmao.clash.util.UiStore
import com.yingmao.clash.view.dialog.RxDialogShapeLoading
@@ -54,16 +50,16 @@ open class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope(), Br
window.decorView.systemUiVisibility =
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
- val decodeBool = MMKV.defaultMMKV().decodeBool(ScreenFlag, false)
- if(decodeBool){
- window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
-
- }else{
- window.setFlags(
- WindowManager.LayoutParams.FLAG_SECURE,
- WindowManager.LayoutParams.FLAG_SECURE
- )
- }
+// val decodeBool = MMKV.defaultMMKV().decodeBool(ScreenFlag, false)
+// if(decodeBool){
+// window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
+//
+// }else{
+// window.setFlags(
+// WindowManager.LayoutParams.FLAG_SECURE,
+// WindowManager.LayoutParams.FLAG_SECURE
+// )
+// }
}
override fun setContentView(layoutResID: Int) {
diff --git a/app/src/main/java/com/yingmao/clash/conf/BZYConstants.kt b/app/src/main/java/com/yingmao/clash/conf/BZYConstants.kt
index 7a88c82..ae33a6c 100644
--- a/app/src/main/java/com/yingmao/clash/conf/BZYConstants.kt
+++ b/app/src/main/java/com/yingmao/clash/conf/BZYConstants.kt
@@ -70,5 +70,6 @@ class BZYConstants {
const val applicationMode = "ApplicationMode"
const val is_splash_first = "is_splash_first"
const val ScreenFlag="screenshotFlag"
+ const val BehaviorLogSwitch = "BehaviorLogSwitch"
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/yingmao/clash/fragment/home/HomeFragment.kt b/app/src/main/java/com/yingmao/clash/fragment/home/HomeFragment.kt
index d77ed1f..b8b10b9 100644
--- a/app/src/main/java/com/yingmao/clash/fragment/home/HomeFragment.kt
+++ b/app/src/main/java/com/yingmao/clash/fragment/home/HomeFragment.kt
@@ -1,6 +1,7 @@
package com.yingmao.clash.fragment.home
import android.annotation.SuppressLint
+import android.app.Activity.RESULT_CANCELED
import android.content.BroadcastReceiver
import android.content.ClipData
import android.content.ClipboardManager
@@ -19,6 +20,7 @@ import android.widget.RelativeLayout
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.result.contract.ActivityResultContracts
+import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity.RESULT_OK
import androidx.appcompat.widget.AppCompatImageView
import androidx.constraintlayout.widget.ConstraintLayout
@@ -49,11 +51,11 @@ import com.yingmao.clash.act.MainActivity.Companion.profileBinder
import com.yingmao.clash.act.MainActivity.Companion.subUrl
import com.yingmao.clash.act.NodeGroupActivity
import com.yingmao.clash.conf.BZYConstants
-import com.yingmao.clash.conf.BZYConstants.Companion.ScreenFlag
import com.yingmao.clash.conf.Conf
import com.yingmao.clash.entity.NodeGroupItem
import com.yingmao.clash.fragment.AccelerateViewModel
import com.yingmao.clash.fragment.BaseFragment
+import com.yingmao.clash.levellog.AppLog
import com.yingmao.clash.net.http.HttpReqType
import com.yingmao.clash.net.http.HttpStatus
import com.yingmao.clash.net.http.entity.AppStartupStatus
@@ -61,6 +63,7 @@ import com.yingmao.clash.net.http.entity.ExpensesRecordRsp
import com.yingmao.clash.net.http.entity.ShowNode
import com.yingmao.clash.util.ActivityResultLifecycle
import com.yingmao.clash.util.CommonUtils
+import com.yingmao.clash.util.Utils
import com.yingmao.clash.util.startClashService
import com.yingmao.clash.util.stopClashService
import com.yingmao.clash.view.RxToast
@@ -127,6 +130,7 @@ open class HomeFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispa
private var connectId = -1
private var baseLoadingView: RxDialogShapeLoading? = null
+ @RequiresApi(Build.VERSION_CODES.P)
override fun init(
titleBarRl: RelativeLayout,
backIv: ImageView,
@@ -200,6 +204,7 @@ open class HomeFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispa
}
}
+ @RequiresApi(Build.VERSION_CODES.P)
private fun initViews(view: View) {
tvUserName = view.findViewById(R.id.tvUserName)
tvUserTimeEnd= view.findViewById(R.id.tvUserTimeEnd)
@@ -219,10 +224,15 @@ open class HomeFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispa
clLinks= view.findViewById(R.id.clLinks)
clNode.setOnClickListener {
+ AppLog.i("HomeFragment", "choose node 选择节点")
if (checkExpire()) {
RxToast.info(resources.getString(R.string.AccountExpired))
return@setOnClickListener
}
+ if (clashRunning) {
+ RxToast.info(resources.getString(R.string.CloseChooseNode))
+ return@setOnClickListener
+ }
if(clashRunning){
val getIsGlo= MMKV.defaultMMKV().decodeBool(BZYConstants.MMKV_KEY_GLOBAL_MODEL, false)
if(getIsGlo){
@@ -239,22 +249,32 @@ open class HomeFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispa
startActivityForResult(intent, 2)
}
tvIntelligentMode.setOnClickListener {
+ if (!clashRunning) {
+ RxToast.info(resources.getString(R.string.OpenSwitchingSourceModel))
+ return@setOnClickListener
+ }
val getIsGlo= MMKV.defaultMMKV().decodeBool(BZYConstants.MMKV_KEY_GLOBAL_MODEL, false)
Log.d("clIntelligentMode ==>${getIsGlo}")
if(getIsGlo){
+ showLoading(resources.getString(R.string.modelchanging), requireActivity())
setProxyModeView(ProxyMode.PAC)
MMKV.defaultMMKV().encode(BZYConstants.MMKV_KEY_GLOBAL_MODEL, false)
- checkGlo()
+ checkGlo(true)
}
}
tvGlobalMode.setOnClickListener {
+ if (!clashRunning) {
+ RxToast.info(resources.getString(R.string.OpenSwitchingSourceModel))
+ return@setOnClickListener
+ }
val getIsGlo= MMKV.defaultMMKV().decodeBool(BZYConstants.MMKV_KEY_GLOBAL_MODEL, false)
Log.d("clGlobalModel ==>${getIsGlo}")
if (!getIsGlo) {
+ showLoading(resources.getString(R.string.modelchanging), requireActivity())
setProxyModeView(ProxyMode.GLOBAL)
MMKV.defaultMMKV().encode(BZYConstants.MMKV_KEY_GLOBAL_MODEL, true)
- checkGlo()
+ checkGlo(true)
}
}
@@ -309,8 +329,15 @@ open class HomeFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispa
}
return@setOnClickListener
}
- showLoading(resources.getString(R.string.Starting), requireContext())
- startClash(true)
+ val networkAvailable = Utils.isNetworkAvailable(requireActivity())
+ if(networkAvailable){
+ showLoading(resources.getString(R.string.Starting), requireContext())
+ startClash(true)
+ //开启完毕之后开启定时器,
+ }else{
+ RxToast.error("网络不可用,请打开网络!")
+ AppLog.i("NewAccFragment", "networkAvailable ${networkAvailable}")
+ }
}
}
aivCustomService.setOnClickListener {
@@ -361,8 +388,11 @@ open class HomeFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispa
}
private fun startClash(isStart: Boolean) {
+ AppLog.i("NewAccFragment", "onClick startClash()")
val nodeJson=serStore.nodes
Log.d("nodeJson: ${nodeJson.isEmpty()}")
+ AppLog.i("NewAccFragment", "subUrl:::${subUrl}")
+ AppLog.i("NewAccFragment", "nodeJson:::${nodeJson}")
if(nodeJson.isEmpty()){
RxToast.info(resources.getString(R.string.GetNodesAgined))
refreshNode()
@@ -402,6 +432,7 @@ open class HomeFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispa
try {
startUTimer()
if (vpnRequest != null) {
+ AppLog.i("NewAccFragment", "vpnRequest::VPN权限未获取 vpnRequest != null")
val result = startActivityForResult(
ActivityResultContracts.StartActivityForResult(), vpnRequest
)
@@ -410,6 +441,14 @@ open class HomeFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispa
stateChanged(CLASH_STARTING, true)
context?.startClashService()
accelerateViewModel.appStartupStatus()
+ AppLog.i("NewAccFragment", "vpnRequest::VPN权限点击同意")
+ }else if(result.resultCode == RESULT_CANCELED){
+ withContext(Dispatchers.Main){
+ hideLoading()
+ stateChanged(CLASH_STOP, false)
+ RxToast.info(resources.getString(R.string.vpnStartingError))
+ AppLog.i("NewAccFragment", "vpnRequest::VPN权限点击取消")
+ }
}
} else {
accelerateViewModel.appStartupStatus()
@@ -461,7 +500,10 @@ open class HomeFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispa
if (isAdded) {
RxToast.success(resources.getString(R.string.AccelerateSuccess))
}
- checkGlo()
+ checkGlo(false)
+ ivAccelerate.setImageResource(R.mipmap.yjl_home_clink_select_bg)
+ AppLog.i("HomeFragment", "收到打开VPN广播 clashRunning==>${clashRunning}")
+
}
Intents.ACTION_CLASH_STOPPED -> {
@@ -469,7 +511,9 @@ open class HomeFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispa
rxDialogShapeLoading.cancel()
}
clashRunning = false
+ ivAccelerate.setImageResource(R.mipmap.yjl_home_clink_bg)
stopUTimer()
+ AppLog.i("HomeFragment", "收到关闭VPN广播 clashRunning==>${clashRunning}")
}
Intents.ACTION_CLASH_REQUEST_STOP -> {
@@ -488,6 +532,7 @@ open class HomeFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispa
val mode = intent.getBooleanExtra("mode", false)
Log.d("ACTION_Glo_Mode mode==>${mode}")
isGlo = mode
+ AppLog.i("HomeFragment", "收到模式切换广播 isGlo==>${isGlo}")
}
}
}
@@ -549,7 +594,7 @@ open class HomeFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispa
} else {
startClash(false)
}
- checkGlo()
+ checkGlo(false)
}?.run {
if (isRunning == true && !clashRunning) {
startClash(false)
@@ -659,13 +704,13 @@ open class HomeFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispa
}
- private fun checkGlo() {
+ private fun checkGlo(isUIChange: Boolean) {
val isGlobal =isGlo
Log.i("checkGlo isGlobal==>${isGlobal} clashRunning==>${clashRunning}")
launch(Dispatchers.IO) {
if (isGlobal) {
if (clashRunning) {
- getNodes()
+ getNodes(isUIChange)
}
}else{
if(clashRunning){
@@ -676,11 +721,9 @@ open class HomeFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispa
if (names.isNotEmpty()) {
var selectedName =
MMKV.defaultMMKV()
- .decodeString(BZYConstants.NODE_SELECTED_NAME_KEY, "")
- if(TextUtils.isEmpty(selectedName)){
- selectedName="自动选择"
- }
+ .decodeString(BZYConstants.NODE_SELECTED_NAME_KEY, "自动选择")
Log.i("selected_name ==>${selectedName}")
+ AppLog.i("NewAccFragment", "selected_name::${selectedName}")
if (!TextUtils.isEmpty(selectedName)) {
val o = cm.queryOverride(Clash.OverrideSlot.Session)
o.mode = TunnelState.Mode.Rule
@@ -693,6 +736,12 @@ open class HomeFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispa
}
}
cm.patchSelector(gName, selectedName!!)
+ if(isUIChange){
+ withContext(Dispatchers.Main){
+ hideLoading()
+ RxToast.info(resources.getString(R.string.modelchangSuccess))
+ }
+ }
}
}
}
@@ -701,7 +750,7 @@ open class HomeFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispa
}
}
- private suspend fun getNodes() {
+ private suspend fun getNodes(isUIChange: Boolean) {
coroutineScope {
val selectedName =
MMKV.defaultMMKV().decodeString(BZYConstants.NODE_SELECTED_NAME_KEY, "")
@@ -740,6 +789,12 @@ open class HomeFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispa
}
cm.patchSelector("GLOBAL", baseNodeName)
+ if(isUIChange){
+ withContext(Dispatchers.Main){
+ hideLoading()
+ RxToast.info(resources.getString(R.string.modelchangSuccess))
+ }
+ }
}
}
}
@@ -769,6 +824,12 @@ open class HomeFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispa
}
cm.patchSelector("GLOBAL", selectedName!!)
+ if(isUIChange){
+ withContext(Dispatchers.Main){
+ hideLoading()
+ RxToast.info(resources.getString(R.string.modelchangSuccess))
+ }
+ }
}
}
}
@@ -820,24 +881,24 @@ open class HomeFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispa
clProxy.visibility=View.VISIBLE
clNode.visibility=View.VISIBLE
}
- val screenshotFlag = it.data?.screenshotFlag
- screenshotFlag.let {
- if (screenshotFlag == true) {
- // 在 Activity 中取消禁止截屏/录屏
- requireActivity().window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
- MMKV.defaultMMKV().encode(
- ScreenFlag, screenshotFlag
- )
- } else {
- requireActivity().window.setFlags(
- WindowManager.LayoutParams.FLAG_SECURE,
- WindowManager.LayoutParams.FLAG_SECURE
- )
- MMKV.defaultMMKV().encode(
- ScreenFlag, screenshotFlag!!
- )
- }
- }
+// val screenshotFlag = it.data?.screenshotFlag
+// screenshotFlag.let {
+// if (screenshotFlag == true) {
+// // 在 Activity 中取消禁止截屏/录屏
+// requireActivity().window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
+// MMKV.defaultMMKV().encode(
+// ScreenFlag, screenshotFlag
+// )
+// } else {
+// requireActivity().window.setFlags(
+// WindowManager.LayoutParams.FLAG_SECURE,
+// WindowManager.LayoutParams.FLAG_SECURE
+// )
+// MMKV.defaultMMKV().encode(
+// ScreenFlag, screenshotFlag!!
+// )
+// }
+// }
}
}
}
diff --git a/app/src/main/java/com/yingmao/clash/fragment/setting/SettingFragment.kt b/app/src/main/java/com/yingmao/clash/fragment/setting/SettingFragment.kt
index 73e94a5..509cfc2 100644
--- a/app/src/main/java/com/yingmao/clash/fragment/setting/SettingFragment.kt
+++ b/app/src/main/java/com/yingmao/clash/fragment/setting/SettingFragment.kt
@@ -1,6 +1,7 @@
package com.yingmao.clash.fragment.setting
import android.annotation.SuppressLint
+import android.app.Application.getProcessName
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
@@ -21,6 +22,8 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
+import com.afollestad.materialdialogs.MaterialDialog
+import com.afollestad.materialdialogs.customview.customView
import com.alibaba.sdk.android.oss.ClientException
import com.alibaba.sdk.android.oss.OSSClient
import com.alibaba.sdk.android.oss.ServiceException
@@ -30,7 +33,9 @@ import com.alibaba.sdk.android.oss.common.auth.OSSStsTokenCredentialProvider
import com.alibaba.sdk.android.oss.internal.OSSAsyncTask
import com.alibaba.sdk.android.oss.model.PutObjectRequest
import com.alibaba.sdk.android.oss.model.PutObjectResult
+import com.cncat.vpn.common.compat.startForegroundServiceCompat
import com.cncat.vpn.common.log.Log
+import com.cncat.vpn.common.util.intent
import com.hjq.permissions.OnPermissionCallback
import com.hjq.permissions.Permission
import com.hjq.permissions.XXPermissions
@@ -42,6 +47,7 @@ import com.yingmao.clash.act.ActivityActivity
import com.yingmao.clash.act.AgreementAndPrivacyActivity
import com.yingmao.clash.act.BandPhoneActivity
import com.yingmao.clash.act.MainActivity
+import com.yingmao.clash.act.MainActivity.Companion.newUuid
import com.yingmao.clash.act.MainActivity.Companion.profileBinder
import com.yingmao.clash.act.MainActivity.Companion.setSub2
import com.yingmao.clash.act.MainActivity.Companion.subUrl
@@ -50,10 +56,12 @@ import com.yingmao.clash.act.SettingActivity
import com.yingmao.clash.act.ShareActivity
import com.yingmao.clash.act.SplashHelper
import com.yingmao.clash.app.MainApplication
+import com.yingmao.clash.levellog.LogcatService
import com.yingmao.clash.conf.BZYConstants
import com.yingmao.clash.entity.SettingPageBean
import com.yingmao.clash.fragment.BaseFragment
import com.yingmao.clash.fragment.home.HomeFragment.Companion.clashRunning
+import com.yingmao.clash.levellog.AppLog
import com.yingmao.clash.net.http.HttpReqType
import com.yingmao.clash.net.http.HttpStatus
import com.yingmao.clash.net.http.entity.ParamData
@@ -108,7 +116,7 @@ class SettingFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispatc
private var baseLoad: RxDialogShapeLoading? = null
private lateinit var mainAtyVM: MainAtyVM
private var settingPageList = mutableListOf()
- @RequiresApi(Build.VERSION_CODES.O)
+ @RequiresApi(Build.VERSION_CODES.P)
override fun init(
titleBarRl: RelativeLayout,
backIv: ImageView,
@@ -125,7 +133,7 @@ class SettingFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispatc
container.addView(view)
}
- @RequiresApi(Build.VERSION_CODES.O)
+ @RequiresApi(Build.VERSION_CODES.P)
@SuppressLint("SetTextI18n")
private fun initData() {
val userName =
@@ -176,12 +184,24 @@ class SettingFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispatc
R.drawable.switching_item_tag,
)
+ val logDirParent = "applogs"
+ val logDir = File(requireActivity().filesDir, logDirParent)
+ val processName =getProcessName()
+ val logFile = File(logDir, "$processName.log")
+ val upLoadData = if(logFile.exists()){
+ SettingPageBean(
+ 7,
+ "上传日志",
+ R.drawable.user_uplog_icon,
+ )
+ }else{
+ SettingPageBean(
+ 7,
+ "采集日志",
+ R.drawable.user_uplog_icon,
+ )
+ }
- val upLoadData= SettingPageBean(
- 7,
- resources.getString(R.string.UploadLog),
- R.drawable.user_uplog_icon,
- )
val privacyPolicyItem= SettingPageBean(
8,
resources.getString(R.string.PrivacyPolicy),
@@ -221,6 +241,7 @@ class SettingFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispatc
if(itemData!=null){
when(itemData.type){
1->{
+ AppLog.i("SettingFragment", "SettingFragment:点击设置")
startActivity(Intent(requireContext(), SettingActivity::class.java))
}
2->{
@@ -273,35 +294,31 @@ class SettingFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispatc
}
7->{
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- if (Environment.isExternalStorageManager()) {
- showLoading(
- resources.getString(R.string.UploadingInProgress),
- requireActivity()
+ if(itemData.name=="上传日志"){
+ showLoading(resources.getString(R.string.UploadingInProgress), requireActivity())
+ mainAtyVM.getUploadParameters()
+ settingPageList[itemData.type-1].name="采集日志"
+ settingAdapter.notifyItemChanged(itemData.type-1)
+ MMKV.defaultMMKV().encode(BZYConstants.BehaviorLogSwitch, false)
+ requireActivity().stopService(LogcatService::class.intent)
+ }else{
+ MaterialDialog(requireActivity()).show {
+ this.cancelable(true)
+ cornerRadius(res = R.dimen.dp_15)
+ customView(
+ R.layout.logsings_dialog_layout,
+ scrollable = false,
+ noVerticalPadding = true,
+ horizontalPadding = false
)
- // 已授予权限
- val directory: File? = requireActivity().getExternalFilesDir("CrashLog")
- if (directory != null) {
- val allFilePath = getAllFilePath(directory.path)
- if (allFilePath.isNotEmpty()) {
- mainAtyVM.getUploadParameters()
- } else {
- android.util.Log.i("checkPermission", "allFilePath is empty")
- hideLoading()
- RxToast.info(resources.getString(R.string.NoErrorLogs))
- }
- } else {
- hideLoading()
- RxToast.error(resources.getString(R.string.NoErrorLogs))
+ val tvSure = findViewById(R.id.tvSure)
+ tvSure.setOnClickListener {
+ settingPageList[itemData.type-1].name="上传日志"
+ settingAdapter.notifyItemChanged(itemData.type-1)
+ requireActivity().startForegroundServiceCompat(LogcatService::class.intent)
+ dismiss()
}
- } else {
- // 未授予权限,跳转到设置页面
- val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
- startActivity(intent)
- hideLoading()
}
- } else {
- checkPermission()
}
}
8->{
@@ -572,6 +589,7 @@ class SettingFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispatc
super.httpLoading(httpStatus)
}
+ @RequiresApi(Build.VERSION_CODES.P)
override fun httpSuccess(httpStatus: HttpStatus.Success) {
when (httpStatus.reqType) {
@@ -621,26 +639,24 @@ class SettingFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispatc
HttpReqType.UploadParameters -> {
val upLoadData = httpStatus.rsp as UploadParamData
if (upLoadData.code == 1000) {
- val directory: File? = requireActivity().getExternalFilesDir("CrashLog")
- //多文件上传
- if (directory != null) {
- val allFilePath = getAllFilePath(directory.path)
- if (upLoadData.data != null) {
- if (allFilePath.isNotEmpty()) {
- val username =
- MMKV.defaultMMKV()
- .decodeString(BZYConstants.MMKV_KEY_REMEMBER_USERNAME, "")!!
- val nowTime = SimpleDateFormat(
- "yyyyMMddkkmmss",
- Locale.getDefault()
- ).format(Date())
- uploadForAliOss(
- upLoadData.data,
- allFilePath,
- "app/errlog/" + username + "_" + nowTime + ".txt"
- )
- }
- }
+ val logDirParent = "applogs"
+ val logDir = File(requireActivity().filesDir, logDirParent)
+ val processName = getProcessName()
+ val logFile = File(logDir, "$processName.log")
+ val username =
+ MMKV.defaultMMKV().decodeString(BZYConstants.MMKV_KEY_REMEMBER_USERNAME, "")!!
+ val nowTime = SimpleDateFormat(
+ "yyyyMMddkkmmss",
+ Locale.getDefault()
+ ).format(Date())
+ if(logFile.exists()){
+ upBehaviorLog(
+ upLoadData.data!!,
+ logFile.path,
+ "app/errlog/"+"${BuildConfig.applicationId}/"+"${username}/" + username + "_" + nowTime + ".txt"
+ )
+ }else{
+ hideLoading()
}
} else {
@@ -831,4 +847,65 @@ class SettingFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispatc
}
})
}
+ private fun upBehaviorLog(data: ParamData, filePath: String, dirStr: String) {
+ val endpoint = "oss-cn-hongkong.aliyuncs.com"
+ val accessKeyId = data.credentials?.accessKeyId
+ val accessKeySecret = data.credentials?.accessKeySecret
+ // 从STS服务获取的安全令牌(SecurityToken)。
+ val securityToken = data.credentials?.securityToken
+ val credentialProvider =
+ OSSStsTokenCredentialProvider(accessKeyId, accessKeySecret, securityToken)
+ val oss = OSSClient(requireActivity(), endpoint, credentialProvider)
+ android.util.Log.i("uploadForAliOss", "filePath::::${filePath} dirStr:::${dirStr} ")
+ val put = PutObjectRequest("yingmao", dirStr, filePath)
+ // 异步上传时可以设置进度回调。
+ put.progressCallback =
+ OSSProgressCallback { request, currentSize, totalSize ->
+ Log.i("progressCallback==> currentSize: $currentSize totalSize: $totalSize")
+ }
+
+ oss.asyncPutObject(put,
+ object : OSSCompletedCallback {
+ override fun onSuccess(request: PutObjectRequest?, result: PutObjectResult) {
+ Log.i("PutObjectRequest result.statusCode: ${result.statusCode}")
+ if (result.statusCode==200) {
+ requireActivity().runOnUiThread {
+ hideLoading()
+ val deleteFile = DataUtils.deleteFile(filePath)
+ Log.i("PutObjectRequest deleteFile ${deleteFile}")
+ AppLog.i("LogcatService", "删除删除成功deleteFile::${deleteFile}")
+ MMKV.defaultMMKV().encode(BZYConstants.BehaviorLogSwitch, false)
+ RxToast.info(resources.getString(R.string.BehaviorLogSuccessfully))
+ }
+ }else{
+ hideLoading()
+ }
+ }
+
+ override fun onFailure(
+ request: PutObjectRequest?,
+ clientExcepion: ClientException,
+ serviceException: ServiceException
+ ) {
+ requireActivity().runOnUiThread {
+ hideLoading()
+ RxToast.error(resources.getString(R.string.BehaviorLogError))
+ }
+ // 请求异常。
+ if (clientExcepion != null) {
+ // 本地异常,如网络异常等。
+ clientExcepion.printStackTrace()
+ }
+ if (serviceException != null) {
+ // 服务异常。
+ android.util.Log.e("ErrorCode", serviceException.errorCode)
+ android.util.Log.e("RequestId", serviceException.requestId)
+ android.util.Log.e("HostId", serviceException.hostId)
+ android.util.Log.e("RawMessage", serviceException.rawMessage)
+ }
+ }
+ })
+
+
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/yingmao/clash/levellog/AppLog.kt b/app/src/main/java/com/yingmao/clash/levellog/AppLog.kt
new file mode 100644
index 0000000..ce9675b
--- /dev/null
+++ b/app/src/main/java/com/yingmao/clash/levellog/AppLog.kt
@@ -0,0 +1,46 @@
+package com.yingmao.clash.levellog
+
+import com.cncat.vpn.common.log.Log
+import com.tencent.mmkv.MMKV
+import com.yingmao.clash.conf.BZYConstants
+
+object AppLog {
+
+ fun v(tag: String, msg: String) =
+ log(LogLevel.VERBOSE, tag, msg, null)
+
+ fun d(tag: String, msg: String) =
+ log(LogLevel.DEBUG, tag, msg, null)
+
+ fun i(tag: String, msg: String) =
+ log(LogLevel.INFO, tag, msg, null)
+
+ fun w(tag: String, msg: String, tr: Throwable? = null) =
+ log(LogLevel.WARN, tag, msg, tr)
+
+ fun e(tag: String, msg: String, tr: Throwable? = null) =
+ log(LogLevel.ERROR, tag, msg, tr)
+
+ private fun log(
+ level: LogLevel,
+ tag: String,
+ msg: String,
+ tr: Throwable?
+ ) {
+ val behaviorLogSwitch =
+ MMKV.defaultMMKV().decodeBool(BZYConstants.BehaviorLogSwitch, false)
+ Log.i("AAAAAAAAAAAA behaviorLogSwitch ${behaviorLogSwitch}")
+ if(behaviorLogSwitch){
+ LogDispatcher.enqueue(
+ LogEvent(
+ time = System.currentTimeMillis(),
+ level = level,
+ tag = tag,
+ message = msg,
+ throwable = tr
+ )
+ )
+ }
+
+ }
+}
diff --git a/app/src/main/java/com/yingmao/clash/levellog/LogDispatcher.kt b/app/src/main/java/com/yingmao/clash/levellog/LogDispatcher.kt
new file mode 100644
index 0000000..6570a91
--- /dev/null
+++ b/app/src/main/java/com/yingmao/clash/levellog/LogDispatcher.kt
@@ -0,0 +1,16 @@
+package com.yingmao.clash.levellog
+
+import kotlinx.coroutines.*
+
+object LogDispatcher {
+
+ private val scope = CoroutineScope(
+ SupervisorJob() + Dispatchers.IO
+ )
+
+ fun enqueue(event: LogEvent) {
+ scope.launch {
+ LogFileWriter.write(event)
+ }
+ }
+}
diff --git a/app/src/main/java/com/yingmao/clash/levellog/LogEvent.kt b/app/src/main/java/com/yingmao/clash/levellog/LogEvent.kt
new file mode 100644
index 0000000..6af4ea1
--- /dev/null
+++ b/app/src/main/java/com/yingmao/clash/levellog/LogEvent.kt
@@ -0,0 +1,9 @@
+package com.yingmao.clash.levellog
+
+data class LogEvent(
+ val time: Long,
+ val level: LogLevel,
+ val tag: String,
+ val message: String,
+ val throwable: Throwable? = null
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/yingmao/clash/levellog/LogFileWriter.kt b/app/src/main/java/com/yingmao/clash/levellog/LogFileWriter.kt
new file mode 100644
index 0000000..1b228a9
--- /dev/null
+++ b/app/src/main/java/com/yingmao/clash/levellog/LogFileWriter.kt
@@ -0,0 +1,133 @@
+package com.yingmao.clash.levellog
+
+import android.app.Application
+import android.content.Context
+import android.os.Build
+import android.os.Process
+import android.util.Log
+import java.io.File
+import java.io.FileWriter
+import java.io.PrintWriter
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+import java.util.concurrent.locks.ReentrantLock
+import kotlin.concurrent.withLock
+
+object LogFileWriter {
+
+ private const val LOG_DIR = "applogs"
+ private const val MAX_FILE_SIZE = 1 * 1024 * 1024 // 1MB
+ private const val MAX_BACKUP_COUNT = 3
+
+ private lateinit var logDir: File
+ private lateinit var logFile: File
+
+ private val lock = ReentrantLock()
+ private val timeFormat =
+ SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.US)
+
+ /**
+ * 必须在 Application.onCreate 调用
+ */
+ fun init(context: Context) {
+ val processName = getProcessName()
+ logDir = File(context.filesDir, LOG_DIR)
+ if (!logDir.exists()) {
+ logDir.mkdirs()
+ }
+
+ logFile = File(logDir, "$processName.log")
+ }
+
+ fun write(event: LogEvent) {
+ if (!::logFile.isInitialized) return
+
+ lock.withLock {
+ rotateIfNeeded()
+
+ FileWriter(logFile, true).use { fw ->
+ PrintWriter(fw).use { pw ->
+ pw.println(format(event))
+ pw.flush()
+ }
+ }
+ }
+ }
+
+ // =====================
+ // 内部实现
+ // =====================
+
+ private fun rotateIfNeeded() {
+ if (!logFile.exists()) return
+ if (logFile.length() < MAX_FILE_SIZE) return
+
+ // 删除最老的
+ val oldest = File(logDir, "${logFile.name}.$MAX_BACKUP_COUNT")
+ if (oldest.exists()) {
+ oldest.delete()
+ }
+
+ // 依次后移
+ for (i in MAX_BACKUP_COUNT - 1 downTo 1) {
+ val src = File(logDir, "${logFile.name}.$i")
+ if (src.exists()) {
+ src.renameTo(File(logDir, "${logFile.name}.${i + 1}"))
+ }
+ }
+
+ // 当前 -> .1
+ logFile.renameTo(File(logDir, "${logFile.name}.1"))
+
+ // 新建空文件
+ logFile = File(logDir, logFile.name)
+ }
+
+ private fun format(event: LogEvent): String {
+ val time = timeFormat.format(Date(event.time))
+ val pid = Process.myPid()
+ val tid = Thread.currentThread().name
+
+ val sb = StringBuilder(256)
+ sb.append("[")
+ .append(time)
+ .append("]")
+ .append("[")
+ .append(event.level.short)
+ .append("]")
+ .append("[pid=").append(pid)
+ .append(",tid=").append(tid)
+ .append("]")
+ .append("[")
+ .append(event.tag)
+ .append("] ")
+ .append(event.message)
+
+ event.throwable?.let {
+ sb.append("\n")
+ sb.append(Log.getStackTraceString(it))
+ }
+
+ return sb.toString()
+ }
+
+ private fun getProcessName(): String {
+ return if (Build.VERSION.SDK_INT >= 28) {
+ Application.getProcessName()
+ } else {
+ readProcName()
+ }
+ }
+
+ private fun readProcName(): String {
+ return try {
+ File("/proc/self/cmdline")
+ .readText()
+ .trim()
+ .replace('\u0000', '_')
+ } catch (e: Exception) {
+ "unknown"
+ }
+ }
+}
diff --git a/app/src/main/java/com/yingmao/clash/levellog/LogLevel.kt b/app/src/main/java/com/yingmao/clash/levellog/LogLevel.kt
new file mode 100644
index 0000000..d99a14f
--- /dev/null
+++ b/app/src/main/java/com/yingmao/clash/levellog/LogLevel.kt
@@ -0,0 +1,9 @@
+package com.yingmao.clash.levellog
+
+enum class LogLevel(val short: String) {
+ VERBOSE("V"),
+ DEBUG("D"),
+ INFO("I"),
+ WARN("W"),
+ ERROR("E")
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/yingmao/clash/levellog/LogcatService.kt b/app/src/main/java/com/yingmao/clash/levellog/LogcatService.kt
new file mode 100644
index 0000000..3e17ae3
--- /dev/null
+++ b/app/src/main/java/com/yingmao/clash/levellog/LogcatService.kt
@@ -0,0 +1,181 @@
+package com.yingmao.clash.levellog
+
+import android.app.PendingIntent
+import android.app.Service
+import android.content.ComponentName
+import android.content.Intent
+import android.content.ServiceConnection
+import android.os.Binder
+import android.os.Build
+import android.os.IBinder
+import android.os.IInterface
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.core.app.NotificationChannelCompat
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import com.cncat.vpn.common.compat.getColorCompat
+import com.cncat.vpn.common.compat.pendingIntentFlags
+import com.cncat.vpn.common.compat.startForegroundCompat
+import com.cncat.vpn.common.util.intent
+import com.cncat.vpn.core.model.LogMessage
+import com.cncat.vpn.service.RemoteService
+import com.cncat.vpn.service.remote.ILogObserver
+import com.cncat.vpn.service.remote.IRemoteService
+import com.cncat.vpn.service.remote.unwrap
+import com.tencent.mmkv.MMKV
+import com.yingmao.clash.R
+import com.yingmao.clash.act.SettingActivity
+import com.yingmao.clash.app.MainApplication
+import com.yingmao.clash.conf.BZYConstants
+import com.yingmao.clash.util.Utils
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.NonCancellable
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.io.IOException
+
+class LogcatService : Service(), CoroutineScope by CoroutineScope(Dispatchers.Default), IInterface {
+ private val connection = object : ServiceConnection {
+ override fun onServiceDisconnected(name: ComponentName?) {
+ Log.i("LogcatServiceClose","onServiceDisconnected::${name}")
+ stopSelf()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.KITKAT)
+ override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
+ Log.i("LogcatServiceConnect","onServiceConnected::${name}")
+ startObserver(service ?: return stopSelf())
+ }
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+
+ running = true
+
+ createNotificationChannel()
+
+ showNotification()
+
+ bindService(RemoteService::class.intent, connection, BIND_AUTO_CREATE)
+ }
+
+ override fun onDestroy() {
+ cancel()
+
+ unbindService(connection)
+
+ stopForeground(true)
+
+ running = false
+
+ super.onDestroy()
+ }
+
+ override fun onBind(intent: Intent?): IBinder {
+ return this.asBinder()
+ }
+
+ override fun asBinder(): IBinder {
+ return object : Binder() {
+ override fun queryLocalInterface(descriptor: String): IInterface {
+ return this@LogcatService
+ }
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.KITKAT)
+ private fun startObserver(binder: IBinder) {
+ if (!binder.isBinderAlive)
+ return stopSelf()
+
+ launch(Dispatchers.IO) {
+ val service = binder.unwrap(IRemoteService::class).clash()
+ val channel = Channel(CACHE_CAPACITY)
+
+ try {
+ MMKV.defaultMMKV().encode(BZYConstants.Companion.BehaviorLogSwitch, true)
+ val selectedName =
+ MMKV.defaultMMKV().decodeString(BZYConstants.Companion.NODE_SELECTED_NAME_KEY, "")
+ val equipmentModel = Utils.getEquipmentModel()
+ AppLog.i("LogcatService", "selectedName===>${selectedName}")
+ AppLog.i("LogcatService", "用户是否过期===>${checkExpire()}")
+ AppLog.i("LogcatService", "机型数据===>${equipmentModel}")
+ AppLog.i("LogcatService", "OAID===>${MainApplication.Companion.OAID}")
+ val observer = object : ILogObserver {
+ override fun newItem(log: LogMessage) {
+ channel.trySend(log)
+ }
+ }
+ service.setLogObserver(observer)
+ while (isActive) {
+ val msg = channel.receive()
+ AppLog.i("LogcatServiceMsg", "core msg===>${msg}")
+ }
+ } catch (e: IOException) {
+ Log.e("Write log file: $e", e.toString())
+ } finally {
+ withContext(NonCancellable) {
+ if (binder.isBinderAlive) {
+ service.setLogObserver(null)
+ }
+ stopSelf()
+ }
+ }
+ }
+ }
+
+ private fun createNotificationChannel() {
+ NotificationManagerCompat.from(this)
+ .createNotificationChannel(
+ NotificationChannelCompat.Builder(
+ CHANNEL_ID,
+ NotificationManagerCompat.IMPORTANCE_DEFAULT
+ ).setName(getString(R.string.clash_logcat)).build()
+ )
+ }
+
+ private fun showNotification() {
+ val notification = NotificationCompat
+ .Builder(this, CHANNEL_ID)
+ .setSmallIcon(R.mipmap.oneclickconnection)
+ .setColor(getColorCompat(R.color.color_clash))
+ .setContentTitle(getString(R.string.clash_logcat))
+ .setContentText("采集中")
+ .setContentIntent(
+ PendingIntent.getActivity(
+ this,
+ R.id.nf_logcat_status,
+ SettingActivity::class.intent
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP),
+ pendingIntentFlags(PendingIntent.FLAG_UPDATE_CURRENT)
+ )
+ )
+ .build()
+
+ startForegroundCompat(R.id.nf_logcat_status, notification)
+ }
+
+ companion object {
+ private const val CHANNEL_ID = "clash_logcat_channel_id"
+ private const val CACHE_CAPACITY = 129
+
+ var running: Boolean = false
+ }
+ private fun checkExpire(): Boolean {
+ val validPeriod = MMKV.defaultMMKV().decodeLong(BZYConstants.Companion.MMKV_KEY_VPN_VALID_PERIOD)
+ if (validPeriod > 0) {
+ val nowTime = System.currentTimeMillis();
+ com.cncat.vpn.common.log.Log.d("validPeriod==>$validPeriod, nowTime==>$nowTime")
+ if (validPeriod >= nowTime) {
+ return false
+ }
+ }
+ return true
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/yingmao/clash/util/Utils.kt b/app/src/main/java/com/yingmao/clash/util/Utils.kt
index 844cdfa..a1e137d 100644
--- a/app/src/main/java/com/yingmao/clash/util/Utils.kt
+++ b/app/src/main/java/com/yingmao/clash/util/Utils.kt
@@ -5,12 +5,17 @@ import android.app.ActivityManager
import android.content.Context
import android.content.pm.PackageManager
import android.media.MediaDrm
+import android.net.ConnectivityManager
+import android.net.NetworkCapabilities
import android.os.Build
import android.provider.Settings
import android.text.TextUtils
import android.util.Log
import android.view.View
import android.view.ViewGroup
+import androidx.annotation.RequiresApi
+import okhttp3.OkHttpClient
+import okhttp3.Request
import java.net.NetworkInterface
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
@@ -18,6 +23,7 @@ import java.security.NoSuchAlgorithmException
import java.util.Arrays
import java.util.Collections
import java.util.UUID
+import java.util.concurrent.TimeUnit
import java.util.regex.Matcher
import java.util.regex.Pattern
import javax.crypto.Cipher
@@ -319,4 +325,56 @@ object Utils {
return String(decrypted, Charsets.UTF_8)
}
+ /**
+ * 是否处于 VPN 网络环境
+ * ✔ API 21+ 可用
+ */
+ fun isVpnActive(context: Context): Boolean {
+ val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+ val networks = cm.allNetworks
+
+ for (network in networks) {
+ val caps = cm.getNetworkCapabilities(network) ?: continue
+ if (caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {
+ return true
+ }
+ }
+ return false
+ }
+ /**
+ * okhttp版 测试
+ */
+
+ fun checkHttpOk(url: String, timeoutMs: Long = 3000): Boolean {
+ return try {
+ val client = OkHttpClient.Builder()
+ .connectTimeout(timeoutMs, TimeUnit.MILLISECONDS)
+ .readTimeout(timeoutMs, TimeUnit.MILLISECONDS)
+ .build()
+
+ val request = Request.Builder()
+ .url(url)
+ .get()
+ .header("Range", "bytes=0-0")
+ .build()
+
+ val response = client.newCall(request).execute()
+ response.isSuccessful || response.code in 300..399
+ } catch (e: Exception) {
+ Log.e("NET_CHECK", "checkHttp failed", e)
+ false
+ }
+ }
+ /**
+ * 是否有可用网络(不保证能上网)
+ */
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ fun isNetworkAvailable(context: Context): Boolean {
+ val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+ val network = cm.activeNetwork ?: return false
+ val caps = cm.getNetworkCapabilities(network) ?: return false
+
+ return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ }
}
\ No newline at end of file
diff --git a/app/src/main/libs/service.aar b/app/src/main/libs/service.aar
index a479c71..32eab3b 100644
Binary files a/app/src/main/libs/service.aar and b/app/src/main/libs/service.aar differ
diff --git a/app/src/main/res/layout/logsings_dialog_layout.xml b/app/src/main/res/layout/logsings_dialog_layout.xml
new file mode 100644
index 0000000..86ec111
--- /dev/null
+++ b/app/src/main/res/layout/logsings_dialog_layout.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 7204d09..d71c4cc 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -12,7 +12,7 @@
HTTP
兼容
%1$s(%2$s)
- Clash 日志捕捉工具
+ 一键连 日志捕捉工具
欢迎回来
用户名
密码
@@ -158,7 +158,7 @@
正在查询支付结果
更新用户信息成功
未查询到已支付,如果长时间未到账请关闭App重新打开.
- 查询失败
+ 查询用户信息失败
获取节点失败:
签到成功 获取赠送时长:%d (分钟)
签到失败
@@ -479,9 +479,21 @@
邀请新朋友通过你的分享链接注册并订购任意会员套餐,\n
您可以获得30天会员(仅限新用户订购有效)
点击分享
- 请开启加速后在进行全局模式切换
+ 请开启加速后在进行模式切换
全局模式加速下暂不支持切换节点,请切换至智能模式!
获取节点中,请稍后点击加速
获取节点信息出错,请手动点击 [源切换] 或联系客服处理!
当前账号尚未绑定,直接退出可能会造成账号丢失,推荐前往绑定账号或保存临时账号后在进行退出
+
+ 1.点击「开始采集行为日志」后,请复现无法使用的问题或正常使用App.
+ \n2.在行为日志采集完成后请再次点击「上传采集行为日志」,提示「行为日志上传成功」后可联系客服人员.
+ \n3.若上传中出现失败等相关问题请联系「客服」
+ \n4.感谢您的支持与信任
+
+ 模式切换中
+ 模式切换成功
+ 行为日志上传成功!
+ 行为日志上传失败!
+ VPN权限未允许 请设置允许!
+ 请关闭加速后在切换节点
\ No newline at end of file