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