一键连新增日志采集功能
This commit is contained in:
parent
d6896c3208
commit
fd6b5d9d2b
|
|
@ -28,6 +28,9 @@
|
||||||
<!-- 适配Android 14以上 google 上架需要注意相关政策 -->
|
<!-- 适配Android 14以上 google 上架需要注意相关政策 -->
|
||||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
<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"/>
|
||||||
|
|
||||||
|
<!--通知权限-->
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
<application
|
<application
|
||||||
android:name=".app.MainApplication"
|
android:name=".app.MainApplication"
|
||||||
android:fullBackupContent="@xml/full_backup_content"
|
android:fullBackupContent="@xml/full_backup_content"
|
||||||
|
|
@ -59,7 +62,7 @@
|
||||||
android:label="${APP_NAME}"
|
android:label="${APP_NAME}"
|
||||||
android:process=":background"
|
android:process=":background"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/SplashTheme">
|
>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
|
@ -143,6 +146,15 @@
|
||||||
android:name=".act.NodeGroupActivity"
|
android:name=".act.NodeGroupActivity"
|
||||||
android:process=":background"
|
android:process=":background"
|
||||||
android:screenOrientation="portrait" />
|
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
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:authorities="${applicationId}.fileprovider"
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,15 @@ import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.widget.AppCompatImageView
|
import androidx.appcompat.widget.AppCompatImageView
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModelProvider
|
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.home.HomeFragment
|
||||||
import com.yingmao.clash.fragment.setting.SettingFragment
|
import com.yingmao.clash.fragment.setting.SettingFragment
|
||||||
import com.yingmao.clash.fragment.vip.vipFragment
|
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.HttpApi
|
||||||
import com.yingmao.clash.net.http.HttpReqType
|
import com.yingmao.clash.net.http.HttpReqType
|
||||||
import com.yingmao.clash.net.http.HttpStatus
|
import com.yingmao.clash.net.http.HttpStatus
|
||||||
|
|
@ -84,9 +88,6 @@ import java.util.UUID
|
||||||
|
|
||||||
|
|
||||||
class MainActivity : BaseActivity() {
|
class MainActivity : BaseActivity() {
|
||||||
|
|
||||||
|
|
||||||
val fragments = arrayListOf<Fragment>()
|
|
||||||
private lateinit var mainAtyVM: MainAtyVM
|
private lateinit var mainAtyVM: MainAtyVM
|
||||||
private var payResultLoading: RxDialogShapeLoading? = null
|
private var payResultLoading: RxDialogShapeLoading? = null
|
||||||
private val clashRunning: Boolean
|
private val clashRunning: Boolean
|
||||||
|
|
@ -161,6 +162,7 @@ class MainActivity : BaseActivity() {
|
||||||
pm.update(it.uuid)
|
pm.update(it.uuid)
|
||||||
newUuid = pro.uuid.toString()
|
newUuid = pro.uuid.toString()
|
||||||
isSubbed = true
|
isSubbed = true
|
||||||
|
AppLog.i("MainActivity", "刷新机场:${newUuid}")
|
||||||
} ?: run {
|
} ?: run {
|
||||||
val uuid: UUID = pm.create(
|
val uuid: UUID = pm.create(
|
||||||
Profile.Type.Url,
|
Profile.Type.Url,
|
||||||
|
|
@ -216,6 +218,23 @@ class MainActivity : BaseActivity() {
|
||||||
startLog()
|
startLog()
|
||||||
firstEntry()
|
firstEntry()
|
||||||
mainAtyVM.getExpensesRecord(mPageInfo)
|
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() {
|
inner class NodeListBR : BroadcastReceiver() {
|
||||||
|
|
@ -266,6 +285,7 @@ class MainActivity : BaseActivity() {
|
||||||
aivHome.setImageResource(R.drawable.yjl_home_page_choose)
|
aivHome.setImageResource(R.drawable.yjl_home_page_choose)
|
||||||
aivCenter.setImageResource(R.drawable.yjl_user_page)
|
aivCenter.setImageResource(R.drawable.yjl_user_page)
|
||||||
aivVip.setImageResource(R.drawable.yjl_vip_page)
|
aivVip.setImageResource(R.drawable.yjl_vip_page)
|
||||||
|
checkVPN()
|
||||||
}
|
}
|
||||||
|
|
||||||
aivCenter.setOnClickListener {
|
aivCenter.setOnClickListener {
|
||||||
|
|
@ -274,6 +294,7 @@ class MainActivity : BaseActivity() {
|
||||||
aivHome.setImageResource(R.drawable.yjl_home_page)
|
aivHome.setImageResource(R.drawable.yjl_home_page)
|
||||||
aivCenter.setImageResource(R.drawable.yjl_user_page_choose)
|
aivCenter.setImageResource(R.drawable.yjl_user_page_choose)
|
||||||
aivVip.setImageResource(R.drawable.yjl_vip_page)
|
aivVip.setImageResource(R.drawable.yjl_vip_page)
|
||||||
|
checkVPN()
|
||||||
}
|
}
|
||||||
|
|
||||||
aivVip.setOnClickListener {
|
aivVip.setOnClickListener {
|
||||||
|
|
@ -334,6 +355,7 @@ class MainActivity : BaseActivity() {
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
subUrl?.let {
|
subUrl?.let {
|
||||||
Log.i("netSubUrl:${netSubUrl}::保存机场URL:=${proSubUrl}::subUrl:${subUrl}")
|
Log.i("netSubUrl:${netSubUrl}::保存机场URL:=${proSubUrl}::subUrl:${subUrl}")
|
||||||
|
AppLog.i("MainActivity", "网络获取机场netSubUrl:${netSubUrl}")
|
||||||
setSub2(it)
|
setSub2(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -376,7 +398,7 @@ class MainActivity : BaseActivity() {
|
||||||
val oldExpireTime =
|
val oldExpireTime =
|
||||||
mmkv.decodeLong(BZYConstants.MMKV_KEY_VPN_VALID_PERIOD, 0)
|
mmkv.decodeLong(BZYConstants.MMKV_KEY_VPN_VALID_PERIOD, 0)
|
||||||
val nowExpireTime = checkLoginRsp.data!!.vpnExpireTime
|
val nowExpireTime = checkLoginRsp.data!!.vpnExpireTime
|
||||||
|
AppLog.i("MainActivity", "刷新有效期:")
|
||||||
if (nowExpireTime!=oldExpireTime) {
|
if (nowExpireTime!=oldExpireTime) {
|
||||||
val expired = checkLoginRsp.data.expired
|
val expired = checkLoginRsp.data.expired
|
||||||
mmkv.encode(BZYConstants.MMKV_KEY_VPN_EXPIRED, expired ?: true)
|
mmkv.encode(BZYConstants.MMKV_KEY_VPN_EXPIRED, expired ?: true)
|
||||||
|
|
@ -397,8 +419,6 @@ class MainActivity : BaseActivity() {
|
||||||
}else if(checkLoginRsp.code == -2){
|
}else if(checkLoginRsp.code == -2){
|
||||||
RxToast.info(resources.getString(R.string.LoginOut))
|
RxToast.info(resources.getString(R.string.LoginOut))
|
||||||
cleanUserInfo()
|
cleanUserInfo()
|
||||||
}else {
|
|
||||||
RxToast.info(resources.getString(R.string.QueryFailed))
|
|
||||||
}
|
}
|
||||||
payResultLoading?.dismiss()
|
payResultLoading?.dismiss()
|
||||||
}
|
}
|
||||||
|
|
@ -505,7 +525,7 @@ class MainActivity : BaseActivity() {
|
||||||
|
|
||||||
HttpReqType.CHECK_LOGIN -> {
|
HttpReqType.CHECK_LOGIN -> {
|
||||||
payResultLoading?.dismiss()
|
payResultLoading?.dismiss()
|
||||||
RxToast.error(resources.getString(R.string.QueryFailed))
|
RxToast.error(resources.getString(R.string.QueryFailed)+ httpStatus.msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpReqType.GET_AIRNODES -> {
|
HttpReqType.GET_AIRNODES -> {
|
||||||
|
|
@ -529,6 +549,7 @@ class MainActivity : BaseActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cleanUserInfo() {
|
fun cleanUserInfo() {
|
||||||
|
AppLog.i("MainActivity", "cleanUserInfo===>退出登录")
|
||||||
MMKV.defaultMMKV().removeValueForKey(BZYConstants.MMKV_KEY_PH_TOKEN)
|
MMKV.defaultMMKV().removeValueForKey(BZYConstants.MMKV_KEY_PH_TOKEN)
|
||||||
MMKV.defaultMMKV().removeValueForKey(BZYConstants.MMKV_KEY_TOKEN)
|
MMKV.defaultMMKV().removeValueForKey(BZYConstants.MMKV_KEY_TOKEN)
|
||||||
MMKV.defaultMMKV().removeValueForKey(BZYConstants.MMKV_KEY_CURRENT_NODE)
|
MMKV.defaultMMKV().removeValueForKey(BZYConstants.MMKV_KEY_CURRENT_NODE)
|
||||||
|
|
@ -783,4 +804,17 @@ class MainActivity : BaseActivity() {
|
||||||
aivCenter.setImageResource(R.drawable.yjl_user_page)
|
aivCenter.setImageResource(R.drawable.yjl_user_page)
|
||||||
aivVip.setImageResource(R.drawable.yjl_vip_page_choose)
|
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}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -44,6 +44,7 @@ import com.yingmao.clash.conf.BZYConstants
|
||||||
import com.yingmao.clash.conf.Conf
|
import com.yingmao.clash.conf.Conf
|
||||||
import com.yingmao.clash.entity.LanguageInfoEntity
|
import com.yingmao.clash.entity.LanguageInfoEntity
|
||||||
import com.yingmao.clash.fragment.home.HomeFragment.Companion.clashRunning
|
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.HttpReqType
|
||||||
import com.yingmao.clash.net.http.HttpStatus
|
import com.yingmao.clash.net.http.HttpStatus
|
||||||
import com.yingmao.clash.net.http.entity.AppVersionCheckRsp
|
import com.yingmao.clash.net.http.entity.AppVersionCheckRsp
|
||||||
|
|
@ -195,6 +196,7 @@ class SettingActivity : BaseActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cleanUserInfo() {
|
private fun cleanUserInfo() {
|
||||||
|
AppLog.i("SettingActivity", "退出登录 cleanUserInfo clashRunning==>${clashRunning}")
|
||||||
MMKV.defaultMMKV().removeValueForKey(BZYConstants.MMKV_KEY_PH_TOKEN)
|
MMKV.defaultMMKV().removeValueForKey(BZYConstants.MMKV_KEY_PH_TOKEN)
|
||||||
MMKV.defaultMMKV().removeValueForKey(BZYConstants.MMKV_KEY_TOKEN)
|
MMKV.defaultMMKV().removeValueForKey(BZYConstants.MMKV_KEY_TOKEN)
|
||||||
MMKV.defaultMMKV().removeValueForKey(BZYConstants.MMKV_KEY_CURRENT_NODE)
|
MMKV.defaultMMKV().removeValueForKey(BZYConstants.MMKV_KEY_CURRENT_NODE)
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ import android.widget.ProgressBar
|
||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
|
|
@ -92,6 +94,12 @@ class SplashActivity : BaseActivity() {
|
||||||
splashAtyVM = ViewModelProvider(this).get(SplashAtyVM::class.java)
|
splashAtyVM = ViewModelProvider(this).get(SplashAtyVM::class.java)
|
||||||
observe(this, splashAtyVM)
|
observe(this, splashAtyVM)
|
||||||
setContentView(R.layout.layout_splash_activity)
|
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)
|
rlMain = findViewById(R.id.rlMain)
|
||||||
rlPb = findViewById(R.id.rl_pb)
|
rlPb = findViewById(R.id.rl_pb)
|
||||||
llAction = findViewById(R.id.ll_action)
|
llAction = findViewById(R.id.ll_action)
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,10 @@ import com.tencent.mmkv.MMKV
|
||||||
import com.yingmao.clash.R
|
import com.yingmao.clash.R
|
||||||
import com.yingmao.clash.conf.Constants
|
import com.yingmao.clash.conf.Constants
|
||||||
import com.yingmao.clash.entity.UserData
|
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.CrashCatchHandler
|
||||||
import com.yingmao.clash.util.clashDir
|
import com.yingmao.clash.util.clashDir
|
||||||
import com.yingmao.clash.view.dialog.DialogSureRestartChoose
|
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.init(this, Constants.USER_ACTION_SET_ID, Constants.APP_SECRET_KEY)
|
||||||
// 设置应用激活的间隔,此方法在初始化成功后才有意义,建议在初始化后执行,具体执行的时机没有限制
|
// 设置应用激活的间隔,此方法在初始化成功后才有意义,建议在初始化后执行,具体执行的时机没有限制
|
||||||
BaiduAction.setActivateInterval(this, 7)
|
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() {
|
private fun extractGeoFiles() {
|
||||||
clashDir.mkdirs()
|
clashDir.mkdirs()
|
||||||
|
|
|
||||||
|
|
@ -18,14 +18,10 @@ import com.yingmao.clash.BuildConfig
|
||||||
import com.yingmao.clash.R
|
import com.yingmao.clash.R
|
||||||
import com.yingmao.clash.conf.BZYConstants
|
import com.yingmao.clash.conf.BZYConstants
|
||||||
import com.yingmao.clash.conf.Conf
|
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.net.http.HttpStatus
|
||||||
import com.yingmao.clash.remote.Broadcasts
|
import com.yingmao.clash.remote.Broadcasts
|
||||||
import com.yingmao.clash.remote.Remote
|
import com.yingmao.clash.remote.Remote
|
||||||
import com.cncat.vpn.service.store.ServiceStore
|
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.StatusBarUtils
|
||||||
import com.yingmao.clash.util.UiStore
|
import com.yingmao.clash.util.UiStore
|
||||||
import com.yingmao.clash.view.dialog.RxDialogShapeLoading
|
import com.yingmao.clash.view.dialog.RxDialogShapeLoading
|
||||||
|
|
@ -54,16 +50,16 @@ open class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope(), Br
|
||||||
window.decorView.systemUiVisibility =
|
window.decorView.systemUiVisibility =
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
|
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
|
||||||
|
|
||||||
val decodeBool = MMKV.defaultMMKV().decodeBool(ScreenFlag, false)
|
// val decodeBool = MMKV.defaultMMKV().decodeBool(ScreenFlag, false)
|
||||||
if(decodeBool){
|
// if(decodeBool){
|
||||||
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
// window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||||
|
//
|
||||||
}else{
|
// }else{
|
||||||
window.setFlags(
|
// window.setFlags(
|
||||||
WindowManager.LayoutParams.FLAG_SECURE,
|
// WindowManager.LayoutParams.FLAG_SECURE,
|
||||||
WindowManager.LayoutParams.FLAG_SECURE
|
// WindowManager.LayoutParams.FLAG_SECURE
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setContentView(layoutResID: Int) {
|
override fun setContentView(layoutResID: Int) {
|
||||||
|
|
|
||||||
|
|
@ -70,5 +70,6 @@ class BZYConstants {
|
||||||
const val applicationMode = "ApplicationMode"
|
const val applicationMode = "ApplicationMode"
|
||||||
const val is_splash_first = "is_splash_first"
|
const val is_splash_first = "is_splash_first"
|
||||||
const val ScreenFlag="screenshotFlag"
|
const val ScreenFlag="screenshotFlag"
|
||||||
|
const val BehaviorLogSwitch = "BehaviorLogSwitch"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.yingmao.clash.fragment.home
|
package com.yingmao.clash.fragment.home
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Activity.RESULT_CANCELED
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
|
|
@ -19,6 +20,7 @@ import android.widget.RelativeLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.activity.result.contract.ActivityResultContract
|
import androidx.activity.result.contract.ActivityResultContract
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AppCompatActivity.RESULT_OK
|
import androidx.appcompat.app.AppCompatActivity.RESULT_OK
|
||||||
import androidx.appcompat.widget.AppCompatImageView
|
import androidx.appcompat.widget.AppCompatImageView
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
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.MainActivity.Companion.subUrl
|
||||||
import com.yingmao.clash.act.NodeGroupActivity
|
import com.yingmao.clash.act.NodeGroupActivity
|
||||||
import com.yingmao.clash.conf.BZYConstants
|
import com.yingmao.clash.conf.BZYConstants
|
||||||
import com.yingmao.clash.conf.BZYConstants.Companion.ScreenFlag
|
|
||||||
import com.yingmao.clash.conf.Conf
|
import com.yingmao.clash.conf.Conf
|
||||||
import com.yingmao.clash.entity.NodeGroupItem
|
import com.yingmao.clash.entity.NodeGroupItem
|
||||||
import com.yingmao.clash.fragment.AccelerateViewModel
|
import com.yingmao.clash.fragment.AccelerateViewModel
|
||||||
import com.yingmao.clash.fragment.BaseFragment
|
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.HttpReqType
|
||||||
import com.yingmao.clash.net.http.HttpStatus
|
import com.yingmao.clash.net.http.HttpStatus
|
||||||
import com.yingmao.clash.net.http.entity.AppStartupStatus
|
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.net.http.entity.ShowNode
|
||||||
import com.yingmao.clash.util.ActivityResultLifecycle
|
import com.yingmao.clash.util.ActivityResultLifecycle
|
||||||
import com.yingmao.clash.util.CommonUtils
|
import com.yingmao.clash.util.CommonUtils
|
||||||
|
import com.yingmao.clash.util.Utils
|
||||||
import com.yingmao.clash.util.startClashService
|
import com.yingmao.clash.util.startClashService
|
||||||
import com.yingmao.clash.util.stopClashService
|
import com.yingmao.clash.util.stopClashService
|
||||||
import com.yingmao.clash.view.RxToast
|
import com.yingmao.clash.view.RxToast
|
||||||
|
|
@ -127,6 +130,7 @@ open class HomeFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispa
|
||||||
private var connectId = -1
|
private var connectId = -1
|
||||||
private var baseLoadingView: RxDialogShapeLoading? = null
|
private var baseLoadingView: RxDialogShapeLoading? = null
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.P)
|
||||||
override fun init(
|
override fun init(
|
||||||
titleBarRl: RelativeLayout,
|
titleBarRl: RelativeLayout,
|
||||||
backIv: ImageView,
|
backIv: ImageView,
|
||||||
|
|
@ -200,6 +204,7 @@ open class HomeFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispa
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.P)
|
||||||
private fun initViews(view: View) {
|
private fun initViews(view: View) {
|
||||||
tvUserName = view.findViewById(R.id.tvUserName)
|
tvUserName = view.findViewById(R.id.tvUserName)
|
||||||
tvUserTimeEnd= view.findViewById(R.id.tvUserTimeEnd)
|
tvUserTimeEnd= view.findViewById(R.id.tvUserTimeEnd)
|
||||||
|
|
@ -219,10 +224,15 @@ open class HomeFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispa
|
||||||
clLinks= view.findViewById(R.id.clLinks)
|
clLinks= view.findViewById(R.id.clLinks)
|
||||||
|
|
||||||
clNode.setOnClickListener {
|
clNode.setOnClickListener {
|
||||||
|
AppLog.i("HomeFragment", "choose node 选择节点")
|
||||||
if (checkExpire()) {
|
if (checkExpire()) {
|
||||||
RxToast.info(resources.getString(R.string.AccountExpired))
|
RxToast.info(resources.getString(R.string.AccountExpired))
|
||||||
return@setOnClickListener
|
return@setOnClickListener
|
||||||
}
|
}
|
||||||
|
if (clashRunning) {
|
||||||
|
RxToast.info(resources.getString(R.string.CloseChooseNode))
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
if(clashRunning){
|
if(clashRunning){
|
||||||
val getIsGlo= MMKV.defaultMMKV().decodeBool(BZYConstants.MMKV_KEY_GLOBAL_MODEL, false)
|
val getIsGlo= MMKV.defaultMMKV().decodeBool(BZYConstants.MMKV_KEY_GLOBAL_MODEL, false)
|
||||||
if(getIsGlo){
|
if(getIsGlo){
|
||||||
|
|
@ -239,22 +249,32 @@ open class HomeFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispa
|
||||||
startActivityForResult(intent, 2)
|
startActivityForResult(intent, 2)
|
||||||
}
|
}
|
||||||
tvIntelligentMode.setOnClickListener {
|
tvIntelligentMode.setOnClickListener {
|
||||||
|
if (!clashRunning) {
|
||||||
|
RxToast.info(resources.getString(R.string.OpenSwitchingSourceModel))
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
val getIsGlo= MMKV.defaultMMKV().decodeBool(BZYConstants.MMKV_KEY_GLOBAL_MODEL, false)
|
val getIsGlo= MMKV.defaultMMKV().decodeBool(BZYConstants.MMKV_KEY_GLOBAL_MODEL, false)
|
||||||
Log.d("clIntelligentMode ==>${getIsGlo}")
|
Log.d("clIntelligentMode ==>${getIsGlo}")
|
||||||
if(getIsGlo){
|
if(getIsGlo){
|
||||||
|
showLoading(resources.getString(R.string.modelchanging), requireActivity())
|
||||||
setProxyModeView(ProxyMode.PAC)
|
setProxyModeView(ProxyMode.PAC)
|
||||||
MMKV.defaultMMKV().encode(BZYConstants.MMKV_KEY_GLOBAL_MODEL, false)
|
MMKV.defaultMMKV().encode(BZYConstants.MMKV_KEY_GLOBAL_MODEL, false)
|
||||||
checkGlo()
|
checkGlo(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
tvGlobalMode.setOnClickListener {
|
tvGlobalMode.setOnClickListener {
|
||||||
|
if (!clashRunning) {
|
||||||
|
RxToast.info(resources.getString(R.string.OpenSwitchingSourceModel))
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
val getIsGlo= MMKV.defaultMMKV().decodeBool(BZYConstants.MMKV_KEY_GLOBAL_MODEL, false)
|
val getIsGlo= MMKV.defaultMMKV().decodeBool(BZYConstants.MMKV_KEY_GLOBAL_MODEL, false)
|
||||||
Log.d("clGlobalModel ==>${getIsGlo}")
|
Log.d("clGlobalModel ==>${getIsGlo}")
|
||||||
if (!getIsGlo) {
|
if (!getIsGlo) {
|
||||||
|
showLoading(resources.getString(R.string.modelchanging), requireActivity())
|
||||||
setProxyModeView(ProxyMode.GLOBAL)
|
setProxyModeView(ProxyMode.GLOBAL)
|
||||||
MMKV.defaultMMKV().encode(BZYConstants.MMKV_KEY_GLOBAL_MODEL, true)
|
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
|
return@setOnClickListener
|
||||||
}
|
}
|
||||||
showLoading(resources.getString(R.string.Starting), requireContext())
|
val networkAvailable = Utils.isNetworkAvailable(requireActivity())
|
||||||
startClash(true)
|
if(networkAvailable){
|
||||||
|
showLoading(resources.getString(R.string.Starting), requireContext())
|
||||||
|
startClash(true)
|
||||||
|
//开启完毕之后开启定时器,
|
||||||
|
}else{
|
||||||
|
RxToast.error("网络不可用,请打开网络!")
|
||||||
|
AppLog.i("NewAccFragment", "networkAvailable ${networkAvailable}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
aivCustomService.setOnClickListener {
|
aivCustomService.setOnClickListener {
|
||||||
|
|
@ -361,8 +388,11 @@ open class HomeFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispa
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startClash(isStart: Boolean) {
|
private fun startClash(isStart: Boolean) {
|
||||||
|
AppLog.i("NewAccFragment", "onClick startClash()")
|
||||||
val nodeJson=serStore.nodes
|
val nodeJson=serStore.nodes
|
||||||
Log.d("nodeJson: ${nodeJson.isEmpty()}")
|
Log.d("nodeJson: ${nodeJson.isEmpty()}")
|
||||||
|
AppLog.i("NewAccFragment", "subUrl:::${subUrl}")
|
||||||
|
AppLog.i("NewAccFragment", "nodeJson:::${nodeJson}")
|
||||||
if(nodeJson.isEmpty()){
|
if(nodeJson.isEmpty()){
|
||||||
RxToast.info(resources.getString(R.string.GetNodesAgined))
|
RxToast.info(resources.getString(R.string.GetNodesAgined))
|
||||||
refreshNode()
|
refreshNode()
|
||||||
|
|
@ -402,6 +432,7 @@ open class HomeFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispa
|
||||||
try {
|
try {
|
||||||
startUTimer()
|
startUTimer()
|
||||||
if (vpnRequest != null) {
|
if (vpnRequest != null) {
|
||||||
|
AppLog.i("NewAccFragment", "vpnRequest::VPN权限未获取 vpnRequest != null")
|
||||||
val result = startActivityForResult(
|
val result = startActivityForResult(
|
||||||
ActivityResultContracts.StartActivityForResult(), vpnRequest
|
ActivityResultContracts.StartActivityForResult(), vpnRequest
|
||||||
)
|
)
|
||||||
|
|
@ -410,6 +441,14 @@ open class HomeFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispa
|
||||||
stateChanged(CLASH_STARTING, true)
|
stateChanged(CLASH_STARTING, true)
|
||||||
context?.startClashService()
|
context?.startClashService()
|
||||||
accelerateViewModel.appStartupStatus()
|
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 {
|
} else {
|
||||||
accelerateViewModel.appStartupStatus()
|
accelerateViewModel.appStartupStatus()
|
||||||
|
|
@ -461,7 +500,10 @@ open class HomeFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispa
|
||||||
if (isAdded) {
|
if (isAdded) {
|
||||||
RxToast.success(resources.getString(R.string.AccelerateSuccess))
|
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 -> {
|
Intents.ACTION_CLASH_STOPPED -> {
|
||||||
|
|
@ -469,7 +511,9 @@ open class HomeFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispa
|
||||||
rxDialogShapeLoading.cancel()
|
rxDialogShapeLoading.cancel()
|
||||||
}
|
}
|
||||||
clashRunning = false
|
clashRunning = false
|
||||||
|
ivAccelerate.setImageResource(R.mipmap.yjl_home_clink_bg)
|
||||||
stopUTimer()
|
stopUTimer()
|
||||||
|
AppLog.i("HomeFragment", "收到关闭VPN广播 clashRunning==>${clashRunning}")
|
||||||
}
|
}
|
||||||
|
|
||||||
Intents.ACTION_CLASH_REQUEST_STOP -> {
|
Intents.ACTION_CLASH_REQUEST_STOP -> {
|
||||||
|
|
@ -488,6 +532,7 @@ open class HomeFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispa
|
||||||
val mode = intent.getBooleanExtra("mode", false)
|
val mode = intent.getBooleanExtra("mode", false)
|
||||||
Log.d("ACTION_Glo_Mode mode==>${mode}")
|
Log.d("ACTION_Glo_Mode mode==>${mode}")
|
||||||
isGlo = mode
|
isGlo = mode
|
||||||
|
AppLog.i("HomeFragment", "收到模式切换广播 isGlo==>${isGlo}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -549,7 +594,7 @@ open class HomeFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispa
|
||||||
} else {
|
} else {
|
||||||
startClash(false)
|
startClash(false)
|
||||||
}
|
}
|
||||||
checkGlo()
|
checkGlo(false)
|
||||||
}?.run {
|
}?.run {
|
||||||
if (isRunning == true && !clashRunning) {
|
if (isRunning == true && !clashRunning) {
|
||||||
startClash(false)
|
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
|
val isGlobal =isGlo
|
||||||
Log.i("checkGlo isGlobal==>${isGlobal} clashRunning==>${clashRunning}")
|
Log.i("checkGlo isGlobal==>${isGlobal} clashRunning==>${clashRunning}")
|
||||||
launch(Dispatchers.IO) {
|
launch(Dispatchers.IO) {
|
||||||
if (isGlobal) {
|
if (isGlobal) {
|
||||||
if (clashRunning) {
|
if (clashRunning) {
|
||||||
getNodes()
|
getNodes(isUIChange)
|
||||||
}
|
}
|
||||||
}else{
|
}else{
|
||||||
if(clashRunning){
|
if(clashRunning){
|
||||||
|
|
@ -676,11 +721,9 @@ open class HomeFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispa
|
||||||
if (names.isNotEmpty()) {
|
if (names.isNotEmpty()) {
|
||||||
var selectedName =
|
var selectedName =
|
||||||
MMKV.defaultMMKV()
|
MMKV.defaultMMKV()
|
||||||
.decodeString(BZYConstants.NODE_SELECTED_NAME_KEY, "")
|
.decodeString(BZYConstants.NODE_SELECTED_NAME_KEY, "自动选择")
|
||||||
if(TextUtils.isEmpty(selectedName)){
|
|
||||||
selectedName="自动选择"
|
|
||||||
}
|
|
||||||
Log.i("selected_name ==>${selectedName}")
|
Log.i("selected_name ==>${selectedName}")
|
||||||
|
AppLog.i("NewAccFragment", "selected_name::${selectedName}")
|
||||||
if (!TextUtils.isEmpty(selectedName)) {
|
if (!TextUtils.isEmpty(selectedName)) {
|
||||||
val o = cm.queryOverride(Clash.OverrideSlot.Session)
|
val o = cm.queryOverride(Clash.OverrideSlot.Session)
|
||||||
o.mode = TunnelState.Mode.Rule
|
o.mode = TunnelState.Mode.Rule
|
||||||
|
|
@ -693,6 +736,12 @@ open class HomeFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispa
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cm.patchSelector(gName, selectedName!!)
|
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 {
|
coroutineScope {
|
||||||
val selectedName =
|
val selectedName =
|
||||||
MMKV.defaultMMKV().decodeString(BZYConstants.NODE_SELECTED_NAME_KEY, "")
|
MMKV.defaultMMKV().decodeString(BZYConstants.NODE_SELECTED_NAME_KEY, "")
|
||||||
|
|
@ -740,6 +789,12 @@ open class HomeFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispa
|
||||||
}
|
}
|
||||||
|
|
||||||
cm.patchSelector("GLOBAL", baseNodeName)
|
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!!)
|
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
|
clProxy.visibility=View.VISIBLE
|
||||||
clNode.visibility=View.VISIBLE
|
clNode.visibility=View.VISIBLE
|
||||||
}
|
}
|
||||||
val screenshotFlag = it.data?.screenshotFlag
|
// val screenshotFlag = it.data?.screenshotFlag
|
||||||
screenshotFlag.let {
|
// screenshotFlag.let {
|
||||||
if (screenshotFlag == true) {
|
// if (screenshotFlag == true) {
|
||||||
// 在 Activity 中取消禁止截屏/录屏
|
// // 在 Activity 中取消禁止截屏/录屏
|
||||||
requireActivity().window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
// requireActivity().window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||||
MMKV.defaultMMKV().encode(
|
// MMKV.defaultMMKV().encode(
|
||||||
ScreenFlag, screenshotFlag
|
// ScreenFlag, screenshotFlag
|
||||||
)
|
// )
|
||||||
} else {
|
// } else {
|
||||||
requireActivity().window.setFlags(
|
// requireActivity().window.setFlags(
|
||||||
WindowManager.LayoutParams.FLAG_SECURE,
|
// WindowManager.LayoutParams.FLAG_SECURE,
|
||||||
WindowManager.LayoutParams.FLAG_SECURE
|
// WindowManager.LayoutParams.FLAG_SECURE
|
||||||
)
|
// )
|
||||||
MMKV.defaultMMKV().encode(
|
// MMKV.defaultMMKV().encode(
|
||||||
ScreenFlag, screenshotFlag!!
|
// ScreenFlag, screenshotFlag!!
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.yingmao.clash.fragment.setting
|
package com.yingmao.clash.fragment.setting
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Application.getProcessName
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
@ -21,6 +22,8 @@ import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
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.ClientException
|
||||||
import com.alibaba.sdk.android.oss.OSSClient
|
import com.alibaba.sdk.android.oss.OSSClient
|
||||||
import com.alibaba.sdk.android.oss.ServiceException
|
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.internal.OSSAsyncTask
|
||||||
import com.alibaba.sdk.android.oss.model.PutObjectRequest
|
import com.alibaba.sdk.android.oss.model.PutObjectRequest
|
||||||
import com.alibaba.sdk.android.oss.model.PutObjectResult
|
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.log.Log
|
||||||
|
import com.cncat.vpn.common.util.intent
|
||||||
import com.hjq.permissions.OnPermissionCallback
|
import com.hjq.permissions.OnPermissionCallback
|
||||||
import com.hjq.permissions.Permission
|
import com.hjq.permissions.Permission
|
||||||
import com.hjq.permissions.XXPermissions
|
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.AgreementAndPrivacyActivity
|
||||||
import com.yingmao.clash.act.BandPhoneActivity
|
import com.yingmao.clash.act.BandPhoneActivity
|
||||||
import com.yingmao.clash.act.MainActivity
|
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.profileBinder
|
||||||
import com.yingmao.clash.act.MainActivity.Companion.setSub2
|
import com.yingmao.clash.act.MainActivity.Companion.setSub2
|
||||||
import com.yingmao.clash.act.MainActivity.Companion.subUrl
|
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.ShareActivity
|
||||||
import com.yingmao.clash.act.SplashHelper
|
import com.yingmao.clash.act.SplashHelper
|
||||||
import com.yingmao.clash.app.MainApplication
|
import com.yingmao.clash.app.MainApplication
|
||||||
|
import com.yingmao.clash.levellog.LogcatService
|
||||||
import com.yingmao.clash.conf.BZYConstants
|
import com.yingmao.clash.conf.BZYConstants
|
||||||
import com.yingmao.clash.entity.SettingPageBean
|
import com.yingmao.clash.entity.SettingPageBean
|
||||||
import com.yingmao.clash.fragment.BaseFragment
|
import com.yingmao.clash.fragment.BaseFragment
|
||||||
import com.yingmao.clash.fragment.home.HomeFragment.Companion.clashRunning
|
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.HttpReqType
|
||||||
import com.yingmao.clash.net.http.HttpStatus
|
import com.yingmao.clash.net.http.HttpStatus
|
||||||
import com.yingmao.clash.net.http.entity.ParamData
|
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 var baseLoad: RxDialogShapeLoading? = null
|
||||||
private lateinit var mainAtyVM: MainAtyVM
|
private lateinit var mainAtyVM: MainAtyVM
|
||||||
private var settingPageList = mutableListOf<SettingPageBean>()
|
private var settingPageList = mutableListOf<SettingPageBean>()
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.P)
|
||||||
override fun init(
|
override fun init(
|
||||||
titleBarRl: RelativeLayout,
|
titleBarRl: RelativeLayout,
|
||||||
backIv: ImageView,
|
backIv: ImageView,
|
||||||
|
|
@ -125,7 +133,7 @@ class SettingFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispatc
|
||||||
container.addView(view)
|
container.addView(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.P)
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
private fun initData() {
|
private fun initData() {
|
||||||
val userName =
|
val userName =
|
||||||
|
|
@ -176,12 +184,24 @@ class SettingFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispatc
|
||||||
R.drawable.switching_item_tag,
|
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(
|
val privacyPolicyItem= SettingPageBean(
|
||||||
8,
|
8,
|
||||||
resources.getString(R.string.PrivacyPolicy),
|
resources.getString(R.string.PrivacyPolicy),
|
||||||
|
|
@ -221,6 +241,7 @@ class SettingFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispatc
|
||||||
if(itemData!=null){
|
if(itemData!=null){
|
||||||
when(itemData.type){
|
when(itemData.type){
|
||||||
1->{
|
1->{
|
||||||
|
AppLog.i("SettingFragment", "SettingFragment:点击设置")
|
||||||
startActivity(Intent(requireContext(), SettingActivity::class.java))
|
startActivity(Intent(requireContext(), SettingActivity::class.java))
|
||||||
}
|
}
|
||||||
2->{
|
2->{
|
||||||
|
|
@ -273,35 +294,31 @@ class SettingFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispatc
|
||||||
|
|
||||||
}
|
}
|
||||||
7->{
|
7->{
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if(itemData.name=="上传日志"){
|
||||||
if (Environment.isExternalStorageManager()) {
|
showLoading(resources.getString(R.string.UploadingInProgress), requireActivity())
|
||||||
showLoading(
|
mainAtyVM.getUploadParameters()
|
||||||
resources.getString(R.string.UploadingInProgress),
|
settingPageList[itemData.type-1].name="采集日志"
|
||||||
requireActivity()
|
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 tvSure = findViewById<TextView>(R.id.tvSure)
|
||||||
val directory: File? = requireActivity().getExternalFilesDir("CrashLog")
|
tvSure.setOnClickListener {
|
||||||
if (directory != null) {
|
settingPageList[itemData.type-1].name="上传日志"
|
||||||
val allFilePath = getAllFilePath(directory.path)
|
settingAdapter.notifyItemChanged(itemData.type-1)
|
||||||
if (allFilePath.isNotEmpty()) {
|
requireActivity().startForegroundServiceCompat(LogcatService::class.intent)
|
||||||
mainAtyVM.getUploadParameters()
|
dismiss()
|
||||||
} 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))
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// 未授予权限,跳转到设置页面
|
|
||||||
val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
|
|
||||||
startActivity(intent)
|
|
||||||
hideLoading()
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
checkPermission()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
8->{
|
8->{
|
||||||
|
|
@ -572,6 +589,7 @@ class SettingFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispatc
|
||||||
super.httpLoading(httpStatus)
|
super.httpLoading(httpStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.P)
|
||||||
override fun httpSuccess(httpStatus: HttpStatus.Success) {
|
override fun httpSuccess(httpStatus: HttpStatus.Success) {
|
||||||
when (httpStatus.reqType) {
|
when (httpStatus.reqType) {
|
||||||
|
|
||||||
|
|
@ -621,26 +639,24 @@ class SettingFragment : BaseFragment(), CoroutineScope by CoroutineScope(Dispatc
|
||||||
HttpReqType.UploadParameters -> {
|
HttpReqType.UploadParameters -> {
|
||||||
val upLoadData = httpStatus.rsp as UploadParamData
|
val upLoadData = httpStatus.rsp as UploadParamData
|
||||||
if (upLoadData.code == 1000) {
|
if (upLoadData.code == 1000) {
|
||||||
val directory: File? = requireActivity().getExternalFilesDir("CrashLog")
|
val logDirParent = "applogs"
|
||||||
//多文件上传
|
val logDir = File(requireActivity().filesDir, logDirParent)
|
||||||
if (directory != null) {
|
val processName = getProcessName()
|
||||||
val allFilePath = getAllFilePath(directory.path)
|
val logFile = File(logDir, "$processName.log")
|
||||||
if (upLoadData.data != null) {
|
val username =
|
||||||
if (allFilePath.isNotEmpty()) {
|
MMKV.defaultMMKV().decodeString(BZYConstants.MMKV_KEY_REMEMBER_USERNAME, "")!!
|
||||||
val username =
|
val nowTime = SimpleDateFormat(
|
||||||
MMKV.defaultMMKV()
|
"yyyyMMddkkmmss",
|
||||||
.decodeString(BZYConstants.MMKV_KEY_REMEMBER_USERNAME, "")!!
|
Locale.getDefault()
|
||||||
val nowTime = SimpleDateFormat(
|
).format(Date())
|
||||||
"yyyyMMddkkmmss",
|
if(logFile.exists()){
|
||||||
Locale.getDefault()
|
upBehaviorLog(
|
||||||
).format(Date())
|
upLoadData.data!!,
|
||||||
uploadForAliOss(
|
logFile.path,
|
||||||
upLoadData.data,
|
"app/errlog/"+"${BuildConfig.applicationId}/"+"${username}/" + username + "_" + nowTime + ".txt"
|
||||||
allFilePath,
|
)
|
||||||
"app/errlog/" + username + "_" + nowTime + ".txt"
|
}else{
|
||||||
)
|
hideLoading()
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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")
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,12 +5,17 @@ import android.app.ActivityManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.media.MediaDrm
|
import android.media.MediaDrm
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
import java.net.NetworkInterface
|
import java.net.NetworkInterface
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
|
@ -18,6 +23,7 @@ import java.security.NoSuchAlgorithmException
|
||||||
import java.util.Arrays
|
import java.util.Arrays
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.regex.Matcher
|
import java.util.regex.Matcher
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
import javax.crypto.Cipher
|
import javax.crypto.Cipher
|
||||||
|
|
@ -319,4 +325,56 @@ object Utils {
|
||||||
|
|
||||||
return String(decrypted, Charsets.UTF_8)
|
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.
|
|
@ -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>
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
<string name="http">HTTP</string>
|
<string name="http">HTTP</string>
|
||||||
<string name="compatible">兼容</string>
|
<string name="compatible">兼容</string>
|
||||||
<string name="format_provider_type">%1$s(%2$s)</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="WelcomeBack">欢迎回来</string>
|
||||||
<string name="username">用户名</string>
|
<string name="username">用户名</string>
|
||||||
<string name="password">密码</string>
|
<string name="password">密码</string>
|
||||||
|
|
@ -158,7 +158,7 @@
|
||||||
<string name="QueryingPaymentResults">正在查询支付结果</string>
|
<string name="QueryingPaymentResults">正在查询支付结果</string>
|
||||||
<string name="SuccessfullyUpdatedUserInformation">更新用户信息成功</string>
|
<string name="SuccessfullyUpdatedUserInformation">更新用户信息成功</string>
|
||||||
<string name="PaymentResultsStr">未查询到已支付,如果长时间未到账请关闭App重新打开.</string>
|
<string name="PaymentResultsStr">未查询到已支付,如果长时间未到账请关闭App重新打开.</string>
|
||||||
<string name="QueryFailed">查询失败</string>
|
<string name="QueryFailed">查询用户信息失败</string>
|
||||||
<string name="NodeAcquisitionFailed">获取节点失败:</string>
|
<string name="NodeAcquisitionFailed">获取节点失败:</string>
|
||||||
<string name="SingInGive">签到成功 获取赠送时长:%d (分钟)</string>
|
<string name="SingInGive">签到成功 获取赠送时长:%d (分钟)</string>
|
||||||
<string name="CheckFailed">签到失败</string>
|
<string name="CheckFailed">签到失败</string>
|
||||||
|
|
@ -479,9 +479,21 @@
|
||||||
<string name="ShartPagetxt">邀请新朋友通过你的分享链接注册并订购任意会员套餐,\n
|
<string name="ShartPagetxt">邀请新朋友通过你的分享链接注册并订购任意会员套餐,\n
|
||||||
您可以获得30天会员(仅限新用户订购有效)</string>
|
您可以获得30天会员(仅限新用户订购有效)</string>
|
||||||
<string name="LinkSharetxt">点击分享</string>
|
<string name="LinkSharetxt">点击分享</string>
|
||||||
<string name="OpenSwitchingSourceModel">请开启加速后在进行全局模式切换</string>
|
<string name="OpenSwitchingSourceModel">请开启加速后在进行模式切换</string>
|
||||||
<string name="GolbalModeNoChoose">全局模式加速下暂不支持切换节点,请切换至智能模式!</string>
|
<string name="GolbalModeNoChoose">全局模式加速下暂不支持切换节点,请切换至智能模式!</string>
|
||||||
<string name="GetNodesAgined">获取节点中,请稍后点击加速</string>
|
<string name="GetNodesAgined">获取节点中,请稍后点击加速</string>
|
||||||
<string name="SubscriptionlinkTop">获取节点信息出错,请手动点击 [源切换] 或联系客服处理!</string>
|
<string name="SubscriptionlinkTop">获取节点信息出错,请手动点击 [源切换] 或联系客服处理!</string>
|
||||||
<string name="bindingShowTxt">当前账号尚未绑定,直接退出可能会造成账号丢失,推荐前往绑定账号或保存临时账号后在进行退出</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>
|
</resources>
|
||||||
Loading…
Reference in New Issue