一键连新增日志采集功能

This commit is contained in:
level 2025-12-25 14:40:44 +08:00
parent d6896c3208
commit fd6b5d9d2b
19 changed files with 842 additions and 110 deletions

View File

@ -28,6 +28,9 @@
<!-- 适配Android 14以上 google 上架需要注意相关政策 -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!--通知权限-->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<application
android:name=".app.MainApplication"
android:fullBackupContent="@xml/full_backup_content"
@ -59,7 +62,7 @@
android:label="${APP_NAME}"
android:process=":background"
android:screenOrientation="portrait"
android:theme="@style/SplashTheme">
>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@ -143,6 +146,15 @@
android:name=".act.NodeGroupActivity"
android:process=":background"
android:screenOrientation="portrait" />
<service
android:name=".levellog.LogcatService"
android:exported="false"
android:process=":background"
android:label="@string/clash_logcat"
android:foregroundServiceType="specialUse">
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="explanation_for_special_use"/>
</service>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"

View File

@ -5,12 +5,15 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.text.TextUtils
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModelProvider
@ -47,6 +50,7 @@ import com.yingmao.clash.entity.NodeGroupItem
import com.yingmao.clash.fragment.home.HomeFragment
import com.yingmao.clash.fragment.setting.SettingFragment
import com.yingmao.clash.fragment.vip.vipFragment
import com.yingmao.clash.levellog.AppLog
import com.yingmao.clash.net.http.HttpApi
import com.yingmao.clash.net.http.HttpReqType
import com.yingmao.clash.net.http.HttpStatus
@ -84,9 +88,6 @@ import java.util.UUID
class MainActivity : BaseActivity() {
val fragments = arrayListOf<Fragment>()
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}")
}
}
}
}

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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) {

View File

@ -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"
}
}

View File

@ -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
}
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!!
// )
// }
// }
}
}
}

View File

@ -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<SettingPageBean>()
@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 upLoadData= SettingPageBean(
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,
resources.getString(R.string.UploadLog),
"上传日志",
R.drawable.user_uplog_icon,
)
}else{
SettingPageBean(
7,
"采集日志",
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()
)
// 已授予权限
val directory: File? = requireActivity().getExternalFilesDir("CrashLog")
if (directory != null) {
val allFilePath = getAllFilePath(directory.path)
if (allFilePath.isNotEmpty()) {
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{
android.util.Log.i("checkPermission", "allFilePath is empty")
hideLoading()
RxToast.info(resources.getString(R.string.NoErrorLogs))
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 tvSure = findViewById<TextView>(R.id.tvSure)
tvSure.setOnClickListener {
settingPageList[itemData.type-1].name="上传日志"
settingAdapter.notifyItemChanged(itemData.type-1)
requireActivity().startForegroundServiceCompat(LogcatService::class.intent)
dismiss()
}
} else {
hideLoading()
RxToast.error(resources.getString(R.string.NoErrorLogs))
}
} 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 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, "")!!
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"
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<PutObjectRequest?, PutObjectResult> {
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)
}
}
})
}
}

View File

@ -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
)
)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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
)

View File

@ -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"
}
}
}

View File

@ -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")
}

View File

@ -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<LogMessage>(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
}
}

View File

@ -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)
}
}

Binary file not shown.

View File

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="@color/translate"
>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:background="@drawable/activity_bg"
>
<TextView
android:id="@+id/tvShowContent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/LogingUseing"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginTop="20dp"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:textSize="16sp"
android:textColor="@color/black"
android:textStyle="bold"
/>
<TextView
android:id="@+id/tvSure"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/tvShowContent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginTop="20dp"
android:layout_marginBottom="20dp"
android:text="@string/AlreadyKnown"
android:paddingStart="40dp"
android:paddingEnd="40dp"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:background="@drawable/pay_sure_click_effect"
android:textColor="@color/blue_4072FE"
android:textSize="16sp"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -12,7 +12,7 @@
<string name="http">HTTP</string>
<string name="compatible">兼容</string>
<string name="format_provider_type">%1$s(%2$s)</string>
<string name="clash_logcat">Clash 日志捕捉工具</string>
<string name="clash_logcat">一键连 日志捕捉工具</string>
<string name="WelcomeBack">欢迎回来</string>
<string name="username">用户名</string>
<string name="password">密码</string>
@ -158,7 +158,7 @@
<string name="QueryingPaymentResults">正在查询支付结果</string>
<string name="SuccessfullyUpdatedUserInformation">更新用户信息成功</string>
<string name="PaymentResultsStr">未查询到已支付,如果长时间未到账请关闭App重新打开.</string>
<string name="QueryFailed">查询失败</string>
<string name="QueryFailed">查询用户信息失败</string>
<string name="NodeAcquisitionFailed">获取节点失败:</string>
<string name="SingInGive">签到成功 获取赠送时长:%d (分钟)</string>
<string name="CheckFailed">签到失败</string>
@ -479,9 +479,21 @@
<string name="ShartPagetxt">邀请新朋友通过你的分享链接注册并订购任意会员套餐,\n
您可以获得30天会员仅限新用户订购有效</string>
<string name="LinkSharetxt">点击分享</string>
<string name="OpenSwitchingSourceModel">请开启加速后在进行全局模式切换</string>
<string name="OpenSwitchingSourceModel">请开启加速后在进行模式切换</string>
<string name="GolbalModeNoChoose">全局模式加速下暂不支持切换节点,请切换至智能模式!</string>
<string name="GetNodesAgined">获取节点中,请稍后点击加速</string>
<string name="SubscriptionlinkTop">获取节点信息出错,请手动点击 [源切换] 或联系客服处理!</string>
<string name="bindingShowTxt">当前账号尚未绑定,直接退出可能会造成账号丢失,推荐前往绑定账号或保存临时账号后在进行退出</string>
<string name="LogingUseing">
1.点击「开始采集行为日志」后,请复现无法使用的问题或正常使用App.
\n2.在行为日志采集完成后请再次点击「上传采集行为日志」,提示「行为日志上传成功」后可联系客服人员.
\n3.若上传中出现失败等相关问题请联系「客服」
\n4.感谢您的支持与信任
</string>
<string name="modelchanging">模式切换中</string>
<string name="modelchangSuccess">模式切换成功</string>
<string name="BehaviorLogSuccessfully">行为日志上传成功!</string>
<string name="BehaviorLogError">行为日志上传失败!</string>
<string name="vpnStartingError">VPN权限未允许 请设置允许!</string>
<string name="CloseChooseNode">请关闭加速后在切换节点</string>
</resources>