Compare commits

...

7 Commits

Author SHA1 Message Date
2dust 66f100ebb3 up 2.0.2 2026-01-16 17:40:34 +08:00
2dust b0213ffdf6 Refactor FAB state handling in MainActivity 2026-01-16 17:40:09 +08:00
2dust 538b2eb0f8 Bug fix
https://github.com/2dust/v2rayNG/issues/5172
2026-01-16 16:36:01 +08:00
2dust f2093c4c52 Refactor real ping test logic into worker service 2026-01-16 16:28:35 +08:00
2dust 02ac57a4c6 Fix and improve port hopping interval handling
https://github.com/2dust/v2rayNG/issues/5171
2026-01-16 15:21:19 +08:00
2dust 379266f205 Add tun inbound to custom config if missing
Updated getV2rayCustomConfig to check for the presence of a 'tun' inbound in the configuration. If not present and HevTun is not used, the method adds a 'tun' inbound from the template configuration and updates the config accordingly.
2026-01-16 15:07:10 +08:00
2dust 23dbb35da7 Refactor MainActivity and clean up SettingsActivity 2026-01-15 17:54:15 +08:00
10 changed files with 232 additions and 362 deletions
+2 -2
View File
@@ -12,8 +12,8 @@ android {
applicationId = "com.v2ray.ang" applicationId = "com.v2ray.ang"
minSdk = 24 minSdk = 24
targetSdk = 36 targetSdk = 36
versionCode = 701 versionCode = 702
versionName = "2.0.1" versionName = "2.0.2"
multiDexEnabled = true multiDexEnabled = true
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';') val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
@@ -4,6 +4,5 @@ data class ConfigResult(
var status: Boolean, var status: Boolean,
var guid: String? = null, var guid: String? = null,
var content: String = "", var content: String = "",
var socksPort: Int? = null,
) )
@@ -168,7 +168,7 @@ inline fun <reified T : Serializable> Intent.serializable(key: String): T? = whe
* *
* @return True if the CharSequence is not null and not empty, false otherwise. * @return True if the CharSequence is not null and not empty, false otherwise.
*/ */
fun CharSequence?.isNotNullEmpty(): Boolean = this != null && this.isNotEmpty() fun CharSequence?.isNotNullEmpty(): Boolean = !this.isNullOrBlank()
fun String.concatUrl(vararg paths: String): String { fun String.concatUrl(vararg paths: String): String {
val builder = StringBuilder(this.trimEnd('/')) val builder = StringBuilder(this.trimEnd('/'))
@@ -3,6 +3,7 @@ package com.v2ray.ang.handler
import android.content.Context import android.content.Context
import android.text.TextUtils import android.text.TextUtils
import android.util.Log import android.util.Log
import com.google.gson.JsonArray
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.ConfigResult import com.v2ray.ang.dto.ConfigResult
import com.v2ray.ang.dto.EConfigType import com.v2ray.ang.dto.EConfigType
@@ -44,7 +45,7 @@ object V2rayConfigManager {
try { try {
val config = MmkvManager.decodeServerConfig(guid) ?: return ConfigResult(false) val config = MmkvManager.decodeServerConfig(guid) ?: return ConfigResult(false)
return if (config.configType == EConfigType.CUSTOM) { return if (config.configType == EConfigType.CUSTOM) {
getV2rayCustomConfig(guid, config) getV2rayCustomConfig(context, guid, config)
} else if (config.configType == EConfigType.POLICYGROUP) { } else if (config.configType == EConfigType.POLICYGROUP) {
getV2rayGroupConfig(context, guid, config) getV2rayGroupConfig(context, guid, config)
} else { } else {
@@ -67,7 +68,7 @@ object V2rayConfigManager {
try { try {
val config = MmkvManager.decodeServerConfig(guid) ?: return ConfigResult(false) val config = MmkvManager.decodeServerConfig(guid) ?: return ConfigResult(false)
return if (config.configType == EConfigType.CUSTOM) { return if (config.configType == EConfigType.CUSTOM) {
getV2rayCustomConfig(guid, config) getV2rayCustomConfig(context, guid, config)
} else if (config.configType == EConfigType.POLICYGROUP) { } else if (config.configType == EConfigType.POLICYGROUP) {
// The number of policy groups will not be very large, so no special handling is needed. // The number of policy groups will not be very large, so no special handling is needed.
getV2rayGroupConfig(context, guid, config) getV2rayGroupConfig(context, guid, config)
@@ -87,9 +88,43 @@ object V2rayConfigManager {
* @param config The profile item containing the configuration details. * @param config The profile item containing the configuration details.
* @return A ConfigResult object containing the result of the configuration retrieval. * @return A ConfigResult object containing the result of the configuration retrieval.
*/ */
private fun getV2rayCustomConfig(guid: String, config: ProfileItem): ConfigResult { private fun getV2rayCustomConfig(context: Context, guid: String, config: ProfileItem): ConfigResult {
val raw = MmkvManager.decodeServerRaw(guid) ?: return ConfigResult(false) val raw = MmkvManager.decodeServerRaw(guid) ?: return ConfigResult(false)
return ConfigResult(true, guid, raw) val result = ConfigResult(true, guid, raw)
if (SettingsManager.isUsingHevTun()) {
return result
}
// check if tun inbound exists
val json = JsonUtil.parseString(raw) ?: return result
val inboundsJson = if (json.has("inbounds") && json.get("inbounds")?.isJsonNull == false) {
json.getAsJsonArray("inbounds")
} else {
JsonArray()
}
for (i in 0 until inboundsJson.size()) {
val elem = inboundsJson.get(i)
if (elem.isJsonObject) {
val inb = elem.asJsonObject
val tag = if (inb.has("tag") && inb.get("tag")?.isJsonNull == false) inb.get("tag").asString else ""
if (tag == "tun") return result
}
}
// add tun inbound from template
val templateConfig = initV2rayConfig(context) ?: return result
val inboundTun = templateConfig.inbounds.firstOrNull { it.tag == "tun" } ?: return result
inboundTun.settings?.mtu = SettingsManager.getVpnMtu()
// add to json
inboundsJson.add(JsonUtil.parseString(JsonUtil.toJson(inboundTun)))
if (inboundsJson.size() == 1) {
json.add("inbounds", inboundsJson)
}
val updatedRaw = JsonUtil.toJsonPretty(json) ?: return result
return ConfigResult(true, guid, updatedRaw)
} }
/** /**
@@ -1192,7 +1227,11 @@ object V2rayConfigManager {
if (profileItem.portHopping.isNotNullEmpty()) { if (profileItem.portHopping.isNotNullEmpty()) {
hysteriaSetting.udphop = StreamSettingsBean.HysteriaSettingsBean.HysteriaUdpHopBean( hysteriaSetting.udphop = StreamSettingsBean.HysteriaSettingsBean.HysteriaUdpHopBean(
port = profileItem.portHopping, port = profileItem.portHopping,
interval = profileItem.portHoppingInterval?.ifEmpty { "30" }.orEmpty().toInt() interval = profileItem.portHoppingInterval
?.trim()
?.toIntOrNull()
?.takeIf { it >= 5 }
?: 30
) )
} }
streamSettings.hysteriaSettings = hysteriaSetting streamSettings.hysteriaSettings = hysteriaSetting
@@ -0,0 +1,85 @@
package com.v2ray.ang.service
import android.content.Context
import com.v2ray.ang.AppConfig
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.handler.V2RayNativeManager
import com.v2ray.ang.handler.V2rayConfigManager
import com.v2ray.ang.util.MessageUtil
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicInteger
/**
* Worker that runs a batch of real-ping tests independently.
* Each batch owns its own CoroutineScope/dispatcher and can be cancelled separately.
*/
class RealPingWorkerService(
private val context: Context,
private val guids: List<String>,
private val onFinish: (status: String) -> Unit = {}
) {
private val job = SupervisorJob()
private val cpu = Runtime.getRuntime().availableProcessors().coerceAtLeast(1)
private val dispatcher = Executors.newFixedThreadPool(cpu * 4).asCoroutineDispatcher()
private val scope = CoroutineScope(job + dispatcher + CoroutineName("RealPingBatchWorker"))
private val runningCount = AtomicInteger(0)
private val totalCount = AtomicInteger(0)
fun start() {
val jobs = guids.map { guid ->
totalCount.incrementAndGet()
scope.launch {
runningCount.incrementAndGet()
try {
val result = startRealPing(guid)
MessageUtil.sendMsg2UI(context, AppConfig.MSG_MEASURE_CONFIG_SUCCESS, Pair(guid, result))
} finally {
val count = totalCount.decrementAndGet()
val left = runningCount.decrementAndGet()
MessageUtil.sendMsg2UI(context, AppConfig.MSG_MEASURE_CONFIG_NOTIFY, "$left / $count")
}
}
}
scope.launch {
try {
joinAll(*jobs.toTypedArray())
onFinish("0")
} catch (_: CancellationException) {
onFinish("-1")
} finally {
close()
}
}
}
fun cancel() {
job.cancel()
}
private fun close() {
try {
dispatcher.close()
} catch (_: Throwable) {
// ignore
}
}
private fun startRealPing(guid: String): Long {
val retFailure = -1L
val configResult = V2rayConfigManager.getV2rayConfig4Speedtest(context, guid)
if (!configResult.status) {
return retFailure
}
return V2RayNativeManager.measureOutboundDelay(configResult.content, SettingsManager.getDelayTestUrl())
}
}
@@ -6,34 +6,15 @@ import android.os.IBinder
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG
import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG_CANCEL import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG_CANCEL
import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG_SUCCESS
import com.v2ray.ang.extension.serializable import com.v2ray.ang.extension.serializable
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.handler.V2RayNativeManager import com.v2ray.ang.handler.V2RayNativeManager
import com.v2ray.ang.handler.V2rayConfigManager
import com.v2ray.ang.util.MessageUtil import com.v2ray.ang.util.MessageUtil
import kotlinx.coroutines.CancellationException import java.util.Collections
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicInteger
class V2RayTestService : Service() { class V2RayTestService : Service() {
private val realTestJob = SupervisorJob()
private val cpu = Runtime.getRuntime().availableProcessors().coerceAtLeast(1)
private val realDispatcher = Executors.newFixedThreadPool((cpu * 2)).asCoroutineDispatcher()
private val realTestScope = CoroutineScope(
realTestJob + realDispatcher + CoroutineName("RealTest")
)
// simple counter for currently running tasks // manage active batch workers so each batch is independent and cancellable
private val realTestRunningCount = AtomicInteger(0) private val activeWorkers = Collections.synchronizedList(mutableListOf<RealPingWorkerService>())
private val realTestCount = AtomicInteger(0)
/** /**
* Initializes the V2Ray environment. * Initializes the V2Ray environment.
@@ -57,8 +38,10 @@ class V2RayTestService : Service() {
*/ */
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
realTestJob.cancel() // cancel any active workers
realDispatcher.close() val snapshot = ArrayList(activeWorkers)
snapshot.forEach { it.cancel() }
activeWorkers.clear()
} }
/** /**
@@ -73,64 +56,24 @@ class V2RayTestService : Service() {
MSG_MEASURE_CONFIG -> { MSG_MEASURE_CONFIG -> {
val guidsList = intent.serializable<ArrayList<String>>("content") val guidsList = intent.serializable<ArrayList<String>>("content")
if (guidsList != null && guidsList.isNotEmpty()) { if (guidsList != null && guidsList.isNotEmpty()) {
startBatchRealPing(guidsList) lateinit var worker: RealPingWorkerService
worker = RealPingWorkerService(this, guidsList) { status ->
// notify UI and remove the worker from active list when finished
MessageUtil.sendMsg2UI(this@V2RayTestService, AppConfig.MSG_MEASURE_CONFIG_FINISH, status)
activeWorkers.remove(worker)
}
activeWorkers.add(worker)
worker.start()
} }
} }
MSG_MEASURE_CONFIG_CANCEL -> { MSG_MEASURE_CONFIG_CANCEL -> {
realTestJob.cancelChildren() // cancel all running batch workers independently
val snapshot = ArrayList(activeWorkers)
snapshot.forEach { it.cancel() }
activeWorkers.clear()
} }
} }
return super.onStartCommand(intent, flags, startId) return super.onStartCommand(intent, flags, startId)
} }
/**
* Starts batch real ping tests.
* @param guidsList The list of GUIDs to test.
*/
private fun startBatchRealPing(guidsList: List<String>) {
val jobs = guidsList.map { guid ->
realTestCount.incrementAndGet()
realTestScope.launch {
realTestRunningCount.incrementAndGet()
try {
val result = startRealPing(guid)
MessageUtil.sendMsg2UI(this@V2RayTestService, MSG_MEASURE_CONFIG_SUCCESS, Pair(guid, result))
} finally {
val count = realTestCount.decrementAndGet()
val left = realTestRunningCount.decrementAndGet()
MessageUtil.sendMsg2UI(this@V2RayTestService, AppConfig.MSG_MEASURE_CONFIG_NOTIFY, "$left / $count")
}
}
}
realTestScope.launch {
try {
joinAll(*jobs.toTypedArray())
notifyAllTasksCompleted("0")
} catch (_: CancellationException) {
notifyAllTasksCompleted("-1")
}
}
}
private fun notifyAllTasksCompleted(status: String) {
MessageUtil.sendMsg2UI(this@V2RayTestService, AppConfig.MSG_MEASURE_CONFIG_FINISH, status)
}
/**
* Starts the real ping test.
* @param guid The GUID of the configuration.
* @return The ping result.
*/
private fun startRealPing(guid: String): Long {
val retFailure = -1L
val configResult = V2rayConfigManager.getV2rayConfig4Speedtest(this, guid)
if (!configResult.status) {
return retFailure
}
return V2RayNativeManager.measureOutboundDelay(configResult.content, SettingsManager.getDelayTestUrl())
}
} }
@@ -115,52 +115,20 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(binding.root) setContentView(binding.root)
setupToolbar(binding.toolbar,false, getString(R.string.title_server)) setupToolbar(binding.toolbar, false, getString(R.string.title_server))
binding.fab.setOnClickListener {
if (mainViewModel.isRunning.value == true) {
V2RayServiceManager.stopVService(this)
} else if ((MmkvManager.decodeSettingsString(AppConfig.PREF_MODE) ?: VPN) == VPN) {
val intent = VpnService.prepare(this)
if (intent == null) {
startV2Ray()
} else {
requestVpnPermission.launch(intent)
}
} else {
startV2Ray()
}
}
binding.layoutTest.setOnClickListener {
if (mainViewModel.isRunning.value == true) {
setTestState(getString(R.string.connection_test_testing))
mainViewModel.testCurrentServerRealPing()
} else {
// tv_test_state.text = getString(R.string.connection_test_fail)
}
}
// setup viewpager and tablayout
groupPagerAdapter = GroupPagerAdapter(this, emptyList()) groupPagerAdapter = GroupPagerAdapter(this, emptyList())
binding.viewPager.adapter = groupPagerAdapter binding.viewPager.adapter = groupPagerAdapter
binding.viewPager.isUserInputEnabled = true binding.viewPager.isUserInputEnabled = true
// setup navigation drawer
val toggle = ActionBarDrawerToggle( val toggle = ActionBarDrawerToggle(
this, binding.drawerLayout, binding.toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close this, binding.drawerLayout, binding.toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close
) )
binding.drawerLayout.addDrawerListener(toggle) binding.drawerLayout.addDrawerListener(toggle)
toggle.syncState() toggle.syncState()
binding.navView.setNavigationItemSelectedListener(this) binding.navView.setNavigationItemSelectedListener(this)
setupGroupTab()
setupViewModel()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
pendingAction = Action.POST_NOTIFICATIONS
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
}
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() { override fun handleOnBackPressed() {
if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) { if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
@@ -172,25 +140,26 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
} }
} }
}) })
binding.fab.setOnClickListener { handleFabAction() }
binding.layoutTest.setOnClickListener { handleLayoutTestClick() }
setupGroupTab()
setupViewModel()
mainViewModel.reloadServerList() mainViewModel.reloadServerList()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
pendingAction = Action.POST_NOTIFICATIONS
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
}
} }
private fun setupViewModel() { private fun setupViewModel() {
mainViewModel.updateTestResultAction.observe(this) { setTestState(it) } mainViewModel.updateTestResultAction.observe(this) { setTestState(it) }
mainViewModel.isRunning.observe(this) { isRunning -> mainViewModel.isRunning.observe(this) { isRunning ->
if (isRunning) { applyRunningState(false, isRunning)
binding.fab.setImageResource(R.drawable.ic_stop_24dp)
binding.fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_fab_active))
binding.fab.contentDescription = getString(R.string.action_stop_service)
setTestState(getString(R.string.connection_connected))
binding.layoutTest.isFocusable = true
} else {
binding.fab.setImageResource(R.drawable.ic_play_24dp)
binding.fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_fab_inactive))
binding.fab.contentDescription = getString(R.string.tasker_start_service)
setTestState(getString(R.string.connection_not_connected))
binding.layoutTest.isFocusable = false
}
} }
mainViewModel.startListenBroadcast() mainViewModel.startListenBroadcast()
mainViewModel.initAssets(assets) mainViewModel.initAssets(assets)
@@ -214,6 +183,32 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
binding.tabGroup.isVisible = groups.size > 1 binding.tabGroup.isVisible = groups.size > 1
} }
private fun handleFabAction() {
applyRunningState(isLoading = true, isRunning = false)
if (mainViewModel.isRunning.value == true) {
V2RayServiceManager.stopVService(this)
} else if ((MmkvManager.decodeSettingsString(AppConfig.PREF_MODE) ?: VPN) == VPN) {
val intent = VpnService.prepare(this)
if (intent == null) {
startV2Ray()
} else {
requestVpnPermission.launch(intent)
}
} else {
startV2Ray()
}
}
private fun handleLayoutTestClick() {
if (mainViewModel.isRunning.value == true) {
setTestState(getString(R.string.connection_test_testing))
mainViewModel.testCurrentServerRealPing()
} else {
// service not running: keep existing no-op (could show a message if desired)
}
}
private fun startV2Ray() { private fun startV2Ray() {
if (MmkvManager.getSelectServer().isNullOrEmpty()) { if (MmkvManager.getSelectServer().isNullOrEmpty()) {
toast(R.string.title_file_chooser) toast(R.string.title_file_chooser)
@@ -232,6 +227,31 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
} }
} }
private fun setTestState(content: String?) {
binding.tvTestState.text = content
}
private fun applyRunningState(isLoading: Boolean, isRunning: Boolean) {
if (isLoading) {
binding.fab.setImageResource(R.drawable.ic_fab_check)
return
}
if (isRunning) {
binding.fab.setImageResource(R.drawable.ic_stop_24dp)
binding.fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_fab_active))
binding.fab.contentDescription = getString(R.string.action_stop_service)
setTestState(getString(R.string.connection_connected))
binding.layoutTest.isFocusable = true
} else {
binding.fab.setImageResource(R.drawable.ic_play_24dp)
binding.fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_fab_inactive))
binding.fab.contentDescription = getString(R.string.tasker_start_service)
setTestState(getString(R.string.connection_not_connected))
binding.layoutTest.isFocusable = false
}
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
} }
@@ -613,19 +633,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
} }
} }
private fun setTestState(content: String?) {
binding.tvTestState.text = content
}
// val mConnection = object : ServiceConnection {
// override fun onServiceDisconnected(name: ComponentName?) {
// }
//
// override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
// sendMsg(AppConfig.MSG_REGISTER_CLIENT, "")
// }
// }
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_BUTTON_B) { if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_BUTTON_B) {
moveTaskToBack(false) moveTaskToBack(false)
@@ -30,6 +30,7 @@ import com.v2ray.ang.helper.ItemTouchHelperAdapter
import com.v2ray.ang.helper.ItemTouchHelperViewHolder import com.v2ray.ang.helper.ItemTouchHelperViewHolder
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.Collections
class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<MainRecyclerAdapter.BaseViewHolder>(), ItemTouchHelperAdapter { class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<MainRecyclerAdapter.BaseViewHolder>(), ItemTouchHelperAdapter {
companion object { companion object {
@@ -282,6 +283,7 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
private fun removeServer(guid: String, position: Int) { private fun removeServer(guid: String, position: Int) {
if (guid == MmkvManager.getSelectServer()) { if (guid == MmkvManager.getSelectServer()) {
application.toast(R.string.toast_action_not_allowed) application.toast(R.string.toast_action_not_allowed)
return
} }
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE)) { if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE)) {
@@ -368,6 +370,9 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean { override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
mActivity.mainViewModel.swapServer(fromPosition, toPosition) mActivity.mainViewModel.swapServer(fromPosition, toPosition)
if (fromPosition < data.size && toPosition < data.size) {
Collections.swap(data, fromPosition, toPosition)
}
notifyItemMoved(fromPosition, toPosition) notifyItemMoved(fromPosition, toPosition)
return true return true
} }
@@ -21,23 +21,18 @@ import com.v2ray.ang.util.Utils
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class SettingsActivity : BaseActivity() { class SettingsActivity : BaseActivity() {
//private val settingsViewModel: SettingsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
//setContentView(R.layout.activity_settings)
setContentViewWithToolbar(R.layout.activity_settings, showHomeAsUp = true, title = getString(R.string.title_settings)) setContentViewWithToolbar(R.layout.activity_settings, showHomeAsUp = true, title = getString(R.string.title_settings))
//settingsViewModel.startListenPreferenceChange()
} }
class SettingsFragment : PreferenceFragmentCompat() { class SettingsFragment : PreferenceFragmentCompat() {
// private val perAppProxy by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_PER_APP_PROXY) }
private val localDns by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_LOCAL_DNS_ENABLED) } private val localDns by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_LOCAL_DNS_ENABLED) }
private val fakeDns by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_FAKE_DNS_ENABLED) } private val fakeDns by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_FAKE_DNS_ENABLED) }
private val appendHttpProxy by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_APPEND_HTTP_PROXY) } private val appendHttpProxy by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_APPEND_HTTP_PROXY) }
// private val localDnsPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_LOCAL_DNS_PORT) }
// private val localDnsPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_LOCAL_DNS_PORT) }
private val vpnDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_VPN_DNS) } private val vpnDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_VPN_DNS) }
private val vpnBypassLan by lazy { findPreference<ListPreference>(AppConfig.PREF_VPN_BYPASS_LAN) } private val vpnBypassLan by lazy { findPreference<ListPreference>(AppConfig.PREF_VPN_BYPASS_LAN) }
private val vpnInterfaceAddress by lazy { findPreference<ListPreference>(AppConfig.PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX) } private val vpnInterfaceAddress by lazy { findPreference<ListPreference>(AppConfig.PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX) }
@@ -55,19 +50,8 @@ class SettingsActivity : BaseActivity() {
private val autoUpdateCheck by lazy { findPreference<CheckBoxPreference>(AppConfig.SUBSCRIPTION_AUTO_UPDATE) } private val autoUpdateCheck by lazy { findPreference<CheckBoxPreference>(AppConfig.SUBSCRIPTION_AUTO_UPDATE) }
private val autoUpdateInterval by lazy { findPreference<EditTextPreference>(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL) } private val autoUpdateInterval by lazy { findPreference<EditTextPreference>(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL) }
// private val socksPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_SOCKS_PORT) }
// private val remoteDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_REMOTE_DNS) }
// private val domesticDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_DOMESTIC_DNS) }
// private val dnsHosts by lazy { findPreference<EditTextPreference>(AppConfig.PREF_DNS_HOSTS) }
// private val delayTestUrl by lazy { findPreference<EditTextPreference>(AppConfig.PREF_DELAY_TEST_URL) }
// private val ipApiUrl by lazy { findPreference<EditTextPreference>(AppConfig.PREF_IP_API_URL) }
private val mode by lazy { findPreference<ListPreference>(AppConfig.PREF_MODE) } private val mode by lazy { findPreference<ListPreference>(AppConfig.PREF_MODE) }
// private val hevTunLogLevel by lazy { findPreference<ListPreference>(AppConfig.PREF_HEV_TUNNEL_LOGLEVEL) }
// private val hevTunRwTimeout by lazy { findPreference<EditTextPreference>(AppConfig.PREF_HEV_TUNNEL_RW_TIMEOUT) }
// private val useTun by lazy { findPreference<ListPreference>(AppConfig.PREF_TUN) }
override fun onCreatePreferences(bundle: Bundle?, s: String?) { override fun onCreatePreferences(bundle: Bundle?, s: String?) {
// Use MMKV as the storage backend for all Preferences // Use MMKV as the storage backend for all Preferences
// This prevents inconsistencies between SharedPreferences and MMKV // This prevents inconsistencies between SharedPreferences and MMKV
@@ -77,30 +61,10 @@ class SettingsActivity : BaseActivity() {
initPreferenceSummaries() initPreferenceSummaries()
// perAppProxy?.setOnPreferenceClickListener {
// startActivity(Intent(activity, PerAppProxyActivity::class.java))
// perAppProxy?.isChecked = true
// false
// }
localDns?.setOnPreferenceChangeListener { _, any -> localDns?.setOnPreferenceChangeListener { _, any ->
updateLocalDns(any as Boolean) updateLocalDns(any as Boolean)
true true
} }
// localDnsPort?.setOnPreferenceChangeListener { _, any ->
// val nval = any as String
// localDnsPort?.summary = nval.ifEmpty { AppConfig.PORT_LOCAL_DNS }
// true
// }
// vpnDns?.setOnPreferenceChangeListener { _, any ->
// vpnDns?.summary = any as String
// true
// }
// vpnMtu?.setOnPreferenceChangeListener { _, any ->
// val nval = any as String
// vpnMtu?.summary = nval.ifEmpty { AppConfig.VPN_MTU.toString() }
// true
// }
mux?.setOnPreferenceChangeListener { _, newValue -> mux?.setOnPreferenceChangeListener { _, newValue ->
updateMux(newValue as Boolean) updateMux(newValue as Boolean)
@@ -119,18 +83,6 @@ class SettingsActivity : BaseActivity() {
updateFragment(newValue as Boolean) updateFragment(newValue as Boolean)
true true
} }
// fragmentPackets?.setOnPreferenceChangeListener { _, newValue ->
// updateFragmentPackets(newValue as String)
// true
// }
// fragmentLength?.setOnPreferenceChangeListener { _, newValue ->
// updateFragmentLength(newValue as String)
// true
// }
// fragmentInterval?.setOnPreferenceChangeListener { _, newValue ->
// updateFragmentInterval(newValue as String)
// true
// }
autoUpdateCheck?.setOnPreferenceChangeListener { _, newValue -> autoUpdateCheck?.setOnPreferenceChangeListener { _, newValue ->
val value = newValue as Boolean val value = newValue as Boolean
@@ -141,65 +93,12 @@ class SettingsActivity : BaseActivity() {
} }
true true
} }
// autoUpdateInterval?.setOnPreferenceChangeListener { _, any ->
// var nval = any as String
//
// // It must be greater than 15 minutes because WorkManager couldn't run tasks under 15 minutes intervals
// nval =
// if (TextUtils.isEmpty(nval) || nval.toLongEx() < 15) AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL else nval
// autoUpdateInterval?.summary = nval
// configureUpdateTask(nval.toLongEx())
// true
// }
// socksPort?.setOnPreferenceChangeListener { _, any ->
// val nval = any as String
// socksPort?.summary = nval.ifEmpty { AppConfig.PORT_SOCKS }
// true
// }
//
// remoteDns?.setOnPreferenceChangeListener { _, any ->
// val nval = any as String
// remoteDns?.summary = nval.ifEmpty { AppConfig.DNS_PROXY }
// true
// }
// domesticDns?.setOnPreferenceChangeListener { _, any ->
// val nval = any as String
// domesticDns?.summary = nval.ifEmpty { AppConfig.DNS_DIRECT }
// true
// }
// dnsHosts?.setOnPreferenceChangeListener { _, any ->
// val nval = any as String
// dnsHosts?.summary = nval
// true
// }
// delayTestUrl?.setOnPreferenceChangeListener { _, any ->
// val nval = any as String
// delayTestUrl?.summary = nval.ifEmpty { AppConfig.DELAY_TEST_URL }
// true
// }
// ipApiUrl?.setOnPreferenceChangeListener { _, any ->
// val nval = any as String
// ipApiUrl?.summary = nval.ifEmpty { AppConfig.IP_API_URL }
// true
// }
mode?.setOnPreferenceChangeListener { _, newValue -> mode?.setOnPreferenceChangeListener { _, newValue ->
updateMode(newValue.toString()) updateMode(newValue.toString())
true true
} }
mode?.dialogLayoutResource = R.layout.preference_with_help_link mode?.dialogLayoutResource = R.layout.preference_with_help_link
//loglevel.summary = "LogLevel"
// useTun?.setOnPreferenceChangeListener { _, newValue ->
// updateHevTunSettings(newValue as String == AppConfig.TUN_hevsocks5)
// true
// }
// hevTunRwTimeout?.setOnPreferenceChangeListener { _, any ->
// val nval = any as String
// hevTunRwTimeout?.summary = nval.ifEmpty { AppConfig.HEVTUN_RW_TIMEOUT }
// true
// }
} }
private fun initPreferenceSummaries() { private fun initPreferenceSummaries() {
@@ -212,6 +111,7 @@ class SettingsActivity : BaseActivity() {
true true
} }
} }
is ListPreference -> { is ListPreference -> {
pref.summary = pref.entry ?: "" pref.summary = pref.entry ?: ""
pref.setOnPreferenceChangeListener { p, newValue -> pref.setOnPreferenceChangeListener { p, newValue ->
@@ -221,6 +121,7 @@ class SettingsActivity : BaseActivity() {
true true
} }
} }
is CheckBoxPreference, is androidx.preference.SwitchPreferenceCompat -> { is CheckBoxPreference, is androidx.preference.SwitchPreferenceCompat -> {
} }
} }
@@ -252,100 +153,10 @@ class SettingsActivity : BaseActivity() {
// Initialize auto-update interval state // Initialize auto-update interval state
autoUpdateInterval?.isEnabled = MmkvManager.decodeSettingsBool(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false) autoUpdateInterval?.isEnabled = MmkvManager.decodeSettingsBool(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
// localDns?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED, false)
// fakeDns?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED, false)
// appendHttpProxy?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_APPEND_HTTP_PROXY, false)
// vpnDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_DNS, AppConfig.DNS_VPN)
// vpnMtu?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_MTU, AppConfig.VPN_MTU.toString())
// mux?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_MUX_ENABLED, false)
// muxConcurrency?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_CONCURRENCY, "8")
// muxXudpConcurrency?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "8")
// fragment?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false)
// fragmentPackets?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_PACKETS, "tlshello")
// fragmentLength?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH, "50-100")
// fragmentInterval?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20")
// autoUpdateCheck?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
// autoUpdateInterval?.summary =
// MmkvManager.decodeSettingsString(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL, AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL)
// socksPort?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_SOCKS_PORT, AppConfig.PORT_SOCKS)
// remoteDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_REMOTE_DNS, AppConfig.DNS_PROXY)
// domesticDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DOMESTIC_DNS, AppConfig.DNS_DIRECT)
// dnsHosts?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DNS_HOSTS)
// delayTestUrl?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL, AppConfig.DELAY_TEST_URL)
// ipApiUrl?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_IP_API_URL, AppConfig.IP_API_URL)
// hevTunRwTimeout?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_HEV_TUNNEL_RW_TIMEOUT, AppConfig.HEVTUN_RW_TIMEOUT)
// updateHevTunSettings(MmkvManager.decodeSettingsString(AppConfig.PREF_TUN, AppConfig.TUN_hevsocks5) == AppConfig.TUN_hevsocks5)
// initSharedPreference()
}
private fun initSharedPreference() {
// listOf(
// //localDnsPort,
// vpnDns,
// vpnMtu,
// muxConcurrency,
// muxXudpConcurrency,
// fragmentLength,
// fragmentInterval,
// autoUpdateInterval,
// socksPort,
// remoteDns,
// domesticDns,
// delayTestUrl,
// ipApiUrl,
// hevTunRwTimeout
// ).forEach { key ->
// key?.summary = key.text.toString()
// }
// listOf(
// AppConfig.PREF_SNIFFING_ENABLED,
// AppConfig.PREF_USE_HEV_TUNNEL
// ).forEach { key ->
// findPreference<CheckBoxPreference>(key)?.isChecked =
// MmkvManager.decodeSettingsBool(key, true)
// }
//
// listOf(
// AppConfig.PREF_ROUTE_ONLY_ENABLED,
// AppConfig.PREF_IS_BOOTED,
// AppConfig.PREF_BYPASS_APPS,
// AppConfig.PREF_SPEED_ENABLED,
// AppConfig.PREF_CONFIRM_REMOVE,
// AppConfig.PREF_START_SCAN_IMMEDIATE,
// AppConfig.PREF_DOUBLE_COLUMN_DISPLAY,
// AppConfig.PREF_PREFER_IPV6,
// AppConfig.PREF_PROXY_SHARING,
// AppConfig.PREF_ALLOW_INSECURE
// ).forEach { key ->
// findPreference<CheckBoxPreference>(key)?.isChecked =
// MmkvManager.decodeSettingsBool(key, false)
// }
//
// listOf(
// AppConfig.PREF_VPN_BYPASS_LAN,
// AppConfig.PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX,
// AppConfig.PREF_ROUTING_DOMAIN_STRATEGY,
// AppConfig.PREF_MUX_XUDP_QUIC,
// AppConfig.PREF_FRAGMENT_PACKETS,
// AppConfig.PREF_LANGUAGE,
// AppConfig.PREF_UI_MODE_NIGHT,
// AppConfig.PREF_LOGLEVEL,
// AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD,
// AppConfig.PREF_MODE,
// AppConfig.PREF_HEV_TUNNEL_LOGLEVEL
// ).forEach { key ->
// if (MmkvManager.decodeSettingsString(key) != null) {
// findPreference<ListPreference>(key)?.value = MmkvManager.decodeSettingsString(key)
// }
// }
} }
private fun updateMode(mode: String?) { private fun updateMode(mode: String?) {
val vpn = mode == VPN val vpn = mode == VPN
// perAppProxy?.isEnabled = vpn
// perAppProxy?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY, false)
localDns?.isEnabled = vpn localDns?.isEnabled = vpn
fakeDns?.isEnabled = vpn fakeDns?.isEnabled = vpn
appendHttpProxy?.isEnabled = vpn appendHttpProxy?.isEnabled = vpn
@@ -423,29 +234,7 @@ class SettingsActivity : BaseActivity() {
fragmentPackets?.isEnabled = enabled fragmentPackets?.isEnabled = enabled
fragmentLength?.isEnabled = enabled fragmentLength?.isEnabled = enabled
fragmentInterval?.isEnabled = enabled fragmentInterval?.isEnabled = enabled
// if (enabled) {
// updateFragmentPackets(MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_PACKETS, "tlshello"))
// updateFragmentLength(MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH, "50-100"))
// updateFragmentInterval(MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20"))
// }
} }
//
// private fun updateFragmentPackets(value: String?) {
// fragmentPackets?.summary = value.toString()
// }
//
// private fun updateFragmentLength(value: String?) {
// fragmentLength?.summary = value.toString()
// }
//
// private fun updateFragmentInterval(value: String?) {
// fragmentInterval?.summary = value.toString()
// }
//
// private fun updateHevTunSettings(enabled: Boolean) {
// hevTunLogLevel?.isEnabled = enabled
// hevTunRwTimeout?.isEnabled = enabled
// }
} }
fun onModeHelpClicked(view: View) { fun onModeHelpClicked(view: View) {
@@ -472,7 +472,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
} }
AppConfig.MSG_MEASURE_CONFIG_FINISH -> { AppConfig.MSG_MEASURE_CONFIG_FINISH -> {
onTestsFinished() val content = intent.getStringExtra("content")
if (content == "0") {
onTestsFinished()
}
} }
} }
} }