Compare commits

...

9 Commits

Author SHA1 Message Date
2dust 80eec036dd up 2.0.14 2026-03-10 19:22:50 +08:00
2dust f9a85366ea Update libs.versions.toml 2026-03-10 17:55:48 +08:00
2dust 378c891399 Update submodule commit SHAs 2026-03-09 20:37:30 +08:00
2dust d04b5eee37 Add detailed logging and validations to services 2026-03-09 20:11:53 +08:00
2dust 405cd7f55e Ensure default subscription on removals
Add removeSubscriptionWithDefault in SettingsManager to remove a subscription and recreate a default subscription if none remain. Modify MmkvManager.decodeAllServerList to include servers from DEFAULT_SUBSCRIPTION_ID when it's not listed, and remove the defensive check in MmkvManager.removeSubscription so removals are delegated to SettingsManager. Update callers (SubEditActivity, SubscriptionsViewModel) to use SettingsManager.removeSubscriptionWithDefault, remove UI restrictions hiding the delete action for the default subscription, and simplify group-display logic in MainViewModel. These changes ensure a default subscription always exists and server lists include default servers when appropriate.
2026-03-09 19:38:29 +08:00
2dust 850096789b When geoip:cn and geoip:private appear in the routing rules, load geoip-only-cn-private.dat to reduce memory usage. 2026-03-09 16:52:57 +08:00
2dust 3d42ac9dba Add logging and safety checks in boot and VPN service
https://github.com/2dust/v2rayNG/issues/5346
2026-03-08 16:11:33 +08:00
2dust d0265265f3 up 2.0.13 2026-03-03 16:11:10 +08:00
2dust 2d6dd33a7b Fix real ping test
https://github.com/2dust/v2rayNG/issues/5331
2026-03-01 17:19:42 +08:00
20 changed files with 234 additions and 79 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 = 712 versionCode = 714
versionName = "2.0.12" versionName = "2.0.14"
multiDexEnabled = true multiDexEnabled = true
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';') val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
@@ -132,6 +132,12 @@ object AppConfig {
const val GEOIP_PRIVATE = "geoip:private" const val GEOIP_PRIVATE = "geoip:private"
const val GEOIP_CN = "geoip:cn" const val GEOIP_CN = "geoip:cn"
/** Geo data file names. */
const val GEOSITE_DAT = "geosite.dat"
const val GEOIP_DAT = "geoip.dat"
const val GEOIP_ONLY_CN_PRIVATE_DAT = "geoip-only-cn-private.dat"
const val GEOIP_ONLY_CN_PRIVATE_URL = "$GITHUB_RAW_URL/Loyalsoldier/geoip/release/$GEOIP_ONLY_CN_PRIVATE_DAT"
/** Ports and addresses for various services. */ /** Ports and addresses for various services. */
const val PORT_LOCAL_DNS = "10853" const val PORT_LOCAL_DNS = "10853"
const val PORT_SOCKS = "10808" const val PORT_SOCKS = "10808"
@@ -0,0 +1,10 @@
package com.v2ray.ang.dto
import java.io.Serializable
data class TestServiceMessage(
val key: Int,
val subscriptionId: String = "",
val serverGuids: List<String> = emptyList()
) : Serializable
@@ -114,9 +114,15 @@ object MmkvManager {
*/ */
fun decodeAllServerList(): MutableList<String> { fun decodeAllServerList(): MutableList<String> {
val allServers = mutableListOf<String>() val allServers = mutableListOf<String>()
val subsList = decodeSubsList()
// Add servers from all subscriptions (including default subscription) // If DEFAULT_SUBSCRIPTION_ID is not in the subscriptions list, add its servers
decodeSubsList().forEach { guid -> if (!subsList.contains(DEFAULT_SUBSCRIPTION_ID)) {
allServers.addAll(decodeServerList(DEFAULT_SUBSCRIPTION_ID))
}
// Add servers from all subscriptions
subsList.forEach { guid ->
allServers.addAll(decodeServerList(guid)) allServers.addAll(decodeServerList(guid))
} }
@@ -381,11 +387,6 @@ object MmkvManager {
* @param subid The subscription ID. * @param subid The subscription ID.
*/ */
fun removeSubscription(subid: String) { fun removeSubscription(subid: String) {
// Protect default subscription from being deleted
if (subid == DEFAULT_SUBSCRIPTION_ID) {
return
}
subStorage.remove(subid) subStorage.remove(subid)
val subsList = decodeSubsList() val subsList = decodeSubsList()
subsList.remove(subid) subsList.remove(subid)
@@ -25,6 +25,7 @@ import com.v2ray.ang.handler.MmkvManager.decodeServerConfig
import com.v2ray.ang.handler.MmkvManager.decodeSubsList import com.v2ray.ang.handler.MmkvManager.decodeSubsList
import com.v2ray.ang.handler.MmkvManager.decodeSubscription import com.v2ray.ang.handler.MmkvManager.decodeSubscription
import com.v2ray.ang.handler.MmkvManager.encodeSubscription import com.v2ray.ang.handler.MmkvManager.encodeSubscription
import com.v2ray.ang.handler.MmkvManager.removeSubscription
import com.v2ray.ang.util.JsonUtil import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import java.io.File import java.io.File
@@ -36,7 +37,7 @@ object SettingsManager {
fun initApp(context: Context) { fun initApp(context: Context) {
ensureDefaultSettings() ensureDefaultSettings()
ensureDefaultSubscription() //ensureDefaultSubscription()
initRoutingRulesets(context) initRoutingRulesets(context)
migrateServerListToSubscriptions() migrateServerListToSubscriptions()
migrateHysteria2PinSHA256() migrateHysteria2PinSHA256()
@@ -240,6 +241,33 @@ object SettingsManager {
.firstOrNull { it.remarks == remarks } .firstOrNull { it.remarks == remarks }
} }
/**
* Removes the subscription.
* If there are no remaining subscriptions,
* it creates a new default subscription to ensure that ungroup
**/
fun removeSubscriptionWithDefault(subid: String) {
// val subsList = decodeSubsList()
// if (subsList.size == 1 && subsList.first() == DEFAULT_SUBSCRIPTION_ID) {
// Log.i(ANG_PACKAGE,"Attempted to remove the only existing default subscription, operation ignored.")
// return
// }
// Remove the subscription
removeSubscription(subid)
// After removal, check if there are any subscriptions left. If not, create a default subscription.
val subsList2 = decodeSubsList()
if (subsList2.isNotEmpty()) {
return
}
val defaultSub = SubscriptionItem(
remarks = "Default",
)
encodeSubscription(DEFAULT_SUBSCRIPTION_ID, defaultSub)
}
/** /**
* Get the SOCKS port. * Get the SOCKS port.
* @return The SOCKS port. * @return The SOCKS port.
@@ -265,7 +293,7 @@ object SettingsManager {
val extFolder = Utils.userAssetPath(context) val extFolder = Utils.userAssetPath(context)
try { try {
val geo = arrayOf("geosite.dat", "geoip.dat") val geo = arrayOf(AppConfig.GEOSITE_DAT, AppConfig.GEOIP_DAT, AppConfig.GEOIP_ONLY_CN_PRIVATE_DAT)
assets.list("") assets.list("")
?.filter { geo.contains(it) } ?.filter { geo.contains(it) }
?.filter { !File(extFolder, it).exists() } ?.filter { !File(extFolder, it).exists() }
@@ -57,9 +57,12 @@ object V2RayServiceManager {
* @param guid The GUID of the server configuration to use (optional). * @param guid The GUID of the server configuration to use (optional).
*/ */
fun startVService(context: Context, guid: String? = null) { fun startVService(context: Context, guid: String? = null) {
Log.i(AppConfig.TAG, "StartCore-Manager: startVService from ${context::class.java.simpleName}")
if (guid != null) { if (guid != null) {
MmkvManager.setSelectServer(guid) MmkvManager.setSelectServer(guid)
} }
startContextService(context) startContextService(context)
} }
@@ -91,15 +94,30 @@ object V2RayServiceManager {
*/ */
private fun startContextService(context: Context) { private fun startContextService(context: Context) {
if (coreController.isRunning) { if (coreController.isRunning) {
Log.w(AppConfig.TAG, "StartCore-Manager: Core already running")
return return
} }
val guid = MmkvManager.getSelectServer() ?: return
val config = MmkvManager.decodeServerConfig(guid) ?: return val guid = MmkvManager.getSelectServer()
if (guid == null) {
Log.e(AppConfig.TAG, "StartCore-Manager: No server selected")
return
}
val config = MmkvManager.decodeServerConfig(guid)
if (config == null) {
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to decode server config")
return
}
if (config.configType != EConfigType.CUSTOM if (config.configType != EConfigType.CUSTOM
&& config.configType != EConfigType.POLICYGROUP && config.configType != EConfigType.POLICYGROUP
&& !Utils.isValidUrl(config.server) && !Utils.isValidUrl(config.server)
&& !Utils.isPureIpAddress(config.server.orEmpty()) && !Utils.isPureIpAddress(config.server.orEmpty())
) return ) {
Log.e(AppConfig.TAG, "StartCore-Manager: Invalid server configuration")
return
}
// val result = V2rayConfigUtil.getV2rayConfig(context, guid) // val result = V2rayConfigUtil.getV2rayConfig(context, guid)
// if (!result.status) return // if (!result.status) return
@@ -108,12 +126,21 @@ object V2RayServiceManager {
} else { } else {
context.toast(R.string.toast_services_start) context.toast(R.string.toast_services_start)
} }
val intent = if (SettingsManager.isVpnMode()) {
val isVpnMode = SettingsManager.isVpnMode()
val intent = if (isVpnMode) {
Log.i(AppConfig.TAG, "StartCore-Manager: Starting VPN service")
Intent(context.applicationContext, V2RayVpnService::class.java) Intent(context.applicationContext, V2RayVpnService::class.java)
} else { } else {
Log.i(AppConfig.TAG, "StartCore-Manager: Starting Proxy service")
Intent(context.applicationContext, V2RayProxyOnlyService::class.java) Intent(context.applicationContext, V2RayProxyOnlyService::class.java)
} }
ContextCompat.startForegroundService(context, intent)
try {
ContextCompat.startForegroundService(context, intent)
} catch (e: Exception) {
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to start service", e)
}
} }
/** /**
@@ -123,15 +150,34 @@ object V2RayServiceManager {
*/ */
fun startCoreLoop(vpnInterface: ParcelFileDescriptor?): Boolean { fun startCoreLoop(vpnInterface: ParcelFileDescriptor?): Boolean {
if (coreController.isRunning) { if (coreController.isRunning) {
Log.w(AppConfig.TAG, "StartCore-Manager: Core already running")
return false return false
} }
val service = getService() ?: return false val service = getService()
val guid = MmkvManager.getSelectServer() ?: return false if (service == null) {
val config = MmkvManager.decodeServerConfig(guid) ?: return false Log.e(AppConfig.TAG, "StartCore-Manager: Service is null")
val result = V2rayConfigManager.getV2rayConfig(service, guid)
if (!result.status)
return false return false
}
val guid = MmkvManager.getSelectServer()
if (guid == null) {
Log.e(AppConfig.TAG, "StartCore-Manager: No server selected")
return false
}
val config = MmkvManager.decodeServerConfig(guid)
if (config == null) {
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to decode server config")
return false
}
Log.i(AppConfig.TAG, "StartCore-Manager: Starting core loop for ${config.remarks}")
val result = V2rayConfigManager.getV2rayConfig(service, guid)
if (!result.status) {
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to get V2Ray config")
return false
}
try { try {
val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_SERVICE) val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_SERVICE)
@@ -140,7 +186,7 @@ object V2RayServiceManager {
mFilter.addAction(Intent.ACTION_USER_PRESENT) mFilter.addAction(Intent.ACTION_USER_PRESENT)
ContextCompat.registerReceiver(service, mMsgReceive, mFilter, Utils.receiverFlags()) ContextCompat.registerReceiver(service, mMsgReceive, mFilter, Utils.receiverFlags())
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to register broadcast receiver", e) Log.e(AppConfig.TAG, "StartCore-Manager: Failed to register receiver", e)
return false return false
} }
@@ -154,11 +200,12 @@ object V2RayServiceManager {
NotificationManager.showNotification(currentConfig) NotificationManager.showNotification(currentConfig)
coreController.startLoop(result.content, tunFd) coreController.startLoop(result.content, tunFd)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to start Core loop", e) Log.e(AppConfig.TAG, "StartCore-Manager: Failed to start core loop", e)
return false return false
} }
if (coreController.isRunning == false) { if (coreController.isRunning == false) {
Log.e(AppConfig.TAG, "StartCore-Manager: Core failed to start")
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "") MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "")
NotificationManager.cancelNotification() NotificationManager.cancelNotification()
return false return false
@@ -166,11 +213,10 @@ object V2RayServiceManager {
try { try {
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "") MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
//NotificationManager.showNotification(currentConfig)
NotificationManager.startSpeedNotification(currentConfig) NotificationManager.startSpeedNotification(currentConfig)
Log.i(AppConfig.TAG, "StartCore-Manager: Core started successfully")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to startup service", e) Log.e(AppConfig.TAG, "StartCore-Manager: Failed to complete startup", e)
return false return false
} }
return true return true
@@ -189,7 +235,7 @@ object V2RayServiceManager {
try { try {
coreController.stopLoop() coreController.stopLoop()
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to stop V2Ray loop", e) Log.e(AppConfig.TAG, "StartCore-Manager: Failed to stop V2Ray loop", e)
} }
} }
} }
@@ -200,7 +246,7 @@ object V2RayServiceManager {
try { try {
service.unregisterReceiver(mMsgReceive) service.unregisterReceiver(mMsgReceive)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to unregister broadcast receiver", e) Log.e(AppConfig.TAG, "StartCore-Manager: Failed to unregister receiver", e)
} }
return true return true
@@ -234,14 +280,14 @@ object V2RayServiceManager {
try { try {
time = coreController.measureDelay(SettingsManager.getDelayTestUrl()) time = coreController.measureDelay(SettingsManager.getDelayTestUrl())
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to measure delay with primary URL", e) Log.e(AppConfig.TAG, "StartCore-Manager: Failed to measure delay", e)
errorStr = e.message?.substringAfter("\":") ?: "empty message" errorStr = e.message?.substringAfter("\":") ?: "empty message"
} }
if (time == -1L) { if (time == -1L) {
try { try {
time = coreController.measureDelay(SettingsManager.getDelayTestUrl(true)) time = coreController.measureDelay(SettingsManager.getDelayTestUrl(true))
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to measure delay with alternative URL", e) Log.e(AppConfig.TAG, "StartCore-Manager: Failed to measure delay", e)
errorStr = e.message?.substringAfter("\":") ?: "empty message" errorStr = e.message?.substringAfter("\":") ?: "empty message"
} }
} }
@@ -293,7 +339,7 @@ object V2RayServiceManager {
serviceControl.stopService() serviceControl.stopService()
0 0
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to stop service in callback", e) Log.e(AppConfig.TAG, "StartCore-Manager: Failed to stop service", e)
-1 -1
} }
} }
@@ -340,12 +386,12 @@ object V2RayServiceManager {
} }
AppConfig.MSG_STATE_STOP -> { AppConfig.MSG_STATE_STOP -> {
Log.i(AppConfig.TAG, "Stop Service") Log.i(AppConfig.TAG, "StartCore-Manager: Stop service")
serviceControl.stopService() serviceControl.stopService()
} }
AppConfig.MSG_STATE_RESTART -> { AppConfig.MSG_STATE_RESTART -> {
Log.i(AppConfig.TAG, "Restart Service") Log.i(AppConfig.TAG, "StartCore-Manager: Restart service")
serviceControl.stopService() serviceControl.stopService()
Thread.sleep(500L) Thread.sleep(500L)
startVService(serviceControl.getService()) startVService(serviceControl.getService())
@@ -358,12 +404,12 @@ object V2RayServiceManager {
when (intent?.action) { when (intent?.action) {
Intent.ACTION_SCREEN_OFF -> { Intent.ACTION_SCREEN_OFF -> {
Log.i(AppConfig.TAG, "SCREEN_OFF, stop querying stats") Log.i(AppConfig.TAG, "StartCore-Manager: Screen off")
NotificationManager.stopSpeedNotification(currentConfig) NotificationManager.stopSpeedNotification(currentConfig)
} }
Intent.ACTION_SCREEN_ON -> { Intent.ACTION_SCREEN_ON -> {
Log.i(AppConfig.TAG, "SCREEN_ON, start querying stats") Log.i(AppConfig.TAG, "StartCore-Manager: Screen on")
NotificationManager.startSpeedNotification(currentConfig) NotificationManager.startSpeedNotification(currentConfig)
} }
} }
@@ -467,6 +467,19 @@ object V2rayConfigManager {
val rule = JsonUtil.fromJson(JsonUtil.toJson(item), RulesBean::class.java) ?: return val rule = JsonUtil.fromJson(JsonUtil.toJson(item), RulesBean::class.java) ?: return
// Replace specific geoip rules with ext versions
rule.ip?.let { ipList ->
val updatedIpList = ArrayList<String>()
ipList.forEach { ip ->
when (ip) {
AppConfig.GEOIP_CN -> updatedIpList.add("ext:${AppConfig.GEOIP_ONLY_CN_PRIVATE_DAT}:cn")
AppConfig.GEOIP_PRIVATE -> updatedIpList.add("ext:${AppConfig.GEOIP_ONLY_CN_PRIVATE_DAT}:private")
else -> updatedIpList.add(ip)
}
}
rule.ip = updatedIpList
}
v2rayConfig.routing.rules.add(rule) v2rayConfig.routing.rules.add(rule)
} catch (e: Exception) { } catch (e: Exception) {
@@ -3,6 +3,8 @@ package com.v2ray.ang.receiver
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.util.Log
import com.v2ray.ang.AppConfig
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.V2RayServiceManager import com.v2ray.ang.handler.V2RayServiceManager
@@ -16,8 +18,24 @@ class BootReceiver : BroadcastReceiver() {
* @param intent The Intent being received. * @param intent The Intent being received.
*/ */
override fun onReceive(context: Context?, intent: Intent?) { override fun onReceive(context: Context?, intent: Intent?) {
if (context == null || intent?.action != Intent.ACTION_BOOT_COMPLETED) return Log.i(AppConfig.TAG, "BootReceiver received: ${intent?.action}")
if (!MmkvManager.decodeStartOnBoot() || MmkvManager.getSelectServer().isNullOrEmpty()) return
if (context == null || intent?.action != Intent.ACTION_BOOT_COMPLETED) {
Log.w(AppConfig.TAG, "BootReceiver: Invalid context or action")
return
}
if (!MmkvManager.decodeStartOnBoot()) {
Log.i(AppConfig.TAG, "BootReceiver: Auto-start on boot is disabled")
return
}
if (MmkvManager.getSelectServer().isNullOrEmpty()) {
Log.w(AppConfig.TAG, "BootReceiver: No server selected")
return
}
Log.i(AppConfig.TAG, "BootReceiver: Starting V2Ray service")
V2RayServiceManager.startVService(context) V2RayServiceManager.startVService(context)
} }
} }
@@ -4,6 +4,8 @@ import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.IBinder import android.os.IBinder
import android.util.Log
import com.v2ray.ang.AppConfig
import com.v2ray.ang.contracts.ServiceControl import com.v2ray.ang.contracts.ServiceControl
import com.v2ray.ang.handler.SettingsManager import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.handler.V2RayServiceManager import com.v2ray.ang.handler.V2RayServiceManager
@@ -16,6 +18,7 @@ class V2RayProxyOnlyService : Service(), ServiceControl {
*/ */
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Log.i(AppConfig.TAG, "StartCore-Proxy: Service created")
V2RayServiceManager.serviceControl = SoftReference(this) V2RayServiceManager.serviceControl = SoftReference(this)
} }
@@ -27,6 +30,7 @@ class V2RayProxyOnlyService : Service(), ServiceControl {
* @return The start mode. * @return The start mode.
*/ */
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.i(AppConfig.TAG, "StartCore-Proxy: Service command received")
V2RayServiceManager.startCoreLoop(null) V2RayServiceManager.startCoreLoop(null)
return START_STICKY return START_STICKY
} }
@@ -6,6 +6,8 @@ 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.dto.TestServiceMessage
import com.v2ray.ang.extension.serializable
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.V2RayNativeManager import com.v2ray.ang.handler.V2RayNativeManager
import com.v2ray.ang.util.MessageUtil import com.v2ray.ang.util.MessageUtil
@@ -52,13 +54,15 @@ class V2RayTestService : Service() {
* @return The start mode. * @return The start mode.
*/ */
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.getIntExtra("key", 0)) { val message = intent?.serializable<TestServiceMessage>("content") ?: return super.onStartCommand(intent, flags, startId)
when (message.key) {
MSG_MEASURE_CONFIG -> { MSG_MEASURE_CONFIG -> {
val subscriptionId = intent.getStringExtra("content").orEmpty() val guidsList = if (message.serverGuids.isNotEmpty()) {
val guidsList = if (subscriptionId.isEmpty()) { message.serverGuids
MmkvManager.decodeAllServerList() } else if (message.subscriptionId.isNotEmpty()) {
MmkvManager.decodeServerList(message.subscriptionId)
} else { } else {
MmkvManager.decodeServerList(subscriptionId) MmkvManager.decodeAllServerList()
} }
if (guidsList.isNotEmpty()) { if (guidsList.isNotEmpty()) {
@@ -1,5 +1,6 @@
package com.v2ray.ang.service package com.v2ray.ang.service
import android.annotation.SuppressLint
import android.app.Service import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@@ -28,6 +29,7 @@ import com.v2ray.ang.util.MyContextWrapper
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import java.lang.ref.SoftReference import java.lang.ref.SoftReference
@SuppressLint("VpnServicePolicy")
class V2RayVpnService : VpnService(), ServiceControl { class V2RayVpnService : VpnService(), ServiceControl {
private lateinit var mInterface: ParcelFileDescriptor private lateinit var mInterface: ParcelFileDescriptor
private var isRunning = false private var isRunning = false
@@ -72,12 +74,14 @@ class V2RayVpnService : VpnService(), ServiceControl {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Log.i(AppConfig.TAG, "StartCore-VPN: Service created")
val policy = StrictMode.ThreadPolicy.Builder().permitAll().build() val policy = StrictMode.ThreadPolicy.Builder().permitAll().build()
StrictMode.setThreadPolicy(policy) StrictMode.setThreadPolicy(policy)
V2RayServiceManager.serviceControl = SoftReference(this) V2RayServiceManager.serviceControl = SoftReference(this)
} }
override fun onRevoke() { override fun onRevoke() {
Log.w(AppConfig.TAG, "StartCore-VPN: Permission revoked")
stopAllService() stopAllService()
} }
@@ -88,10 +92,12 @@ class V2RayVpnService : VpnService(), ServiceControl {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
Log.i(AppConfig.TAG, "StartCore-VPN: Service destroyed")
NotificationManager.cancelNotification() NotificationManager.cancelNotification()
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.i(AppConfig.TAG, "StartCore-VPN: Service command received")
setupVpnService() setupVpnService()
startService() startService()
return START_STICKY return START_STICKY
@@ -103,12 +109,12 @@ class V2RayVpnService : VpnService(), ServiceControl {
} }
override fun startService() { override fun startService() {
if (mInterface == null) { if (!::mInterface.isInitialized) {
Log.e(AppConfig.TAG, "Failed to create VPN interface") Log.e(AppConfig.TAG, "StartCore-VPN: Interface not initialized")
return return
} }
if (!V2RayServiceManager.startCoreLoop(mInterface)) { if (!V2RayServiceManager.startCoreLoop(mInterface)) {
Log.e(AppConfig.TAG, "Failed to start V2Ray core loop") Log.e(AppConfig.TAG, "StartCore-VPN: Failed to start core loop")
stopAllService() stopAllService()
return return
} }
@@ -136,13 +142,13 @@ class V2RayVpnService : VpnService(), ServiceControl {
private fun setupVpnService() { private fun setupVpnService() {
val prepare = prepare(this) val prepare = prepare(this)
if (prepare != null) { if (prepare != null) {
Log.e(AppConfig.TAG, "VPN preparation failed") Log.e(AppConfig.TAG, "StartCore-VPN: Permission not granted")
stopSelf() stopSelf()
return return
} }
if (configureVpnService() != true) { if (configureVpnService() != true) {
Log.e(AppConfig.TAG, "VPN configuration failed") Log.e(AppConfig.TAG, "StartCore-VPN: Configuration failed")
stopSelf() stopSelf()
return return
} }
@@ -165,9 +171,11 @@ class V2RayVpnService : VpnService(), ServiceControl {
// Close the old interface since the parameters have been changed // Close the old interface since the parameters have been changed
try { try {
mInterface.close() if (::mInterface.isInitialized) {
} catch (ignored: Exception) { mInterface.close()
// ignored }
} catch (e: Exception) {
Log.w(AppConfig.TAG, "Failed to close old interface", e)
} }
// Configure platform-specific features // Configure platform-specific features
@@ -244,7 +252,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
try { try {
connectivity.requestNetwork(defaultNetworkRequest, defaultNetworkCallback) connectivity.requestNetwork(defaultNetworkRequest, defaultNetworkCallback)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to request default network", e) Log.e(AppConfig.TAG, "StartCore-VPN: Failed to request network", e)
} }
} }
@@ -297,7 +305,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
builder.addAllowedApplication(it) builder.addAllowedApplication(it)
} }
} catch (e: PackageManager.NameNotFoundException) { } catch (e: PackageManager.NameNotFoundException) {
Log.e(AppConfig.TAG, "Failed to configure app in VPN: ${e.localizedMessage}", e) Log.e(AppConfig.TAG, "StartCore-VPN: Failed to configure app", e)
} }
} }
} }
@@ -330,8 +338,8 @@ class V2RayVpnService : VpnService(), ServiceControl {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
try { try {
connectivity.unregisterNetworkCallback(defaultNetworkCallback) connectivity.unregisterNetworkCallback(defaultNetworkCallback)
} catch (ignored: Exception) { } catch (e: Exception) {
// ignored Log.w(AppConfig.TAG, "StartCore-VPN: Failed to unregister callback", e)
} }
} }
@@ -349,9 +357,11 @@ class V2RayVpnService : VpnService(), ServiceControl {
stopSelf() stopSelf()
try { try {
mInterface.close() if (::mInterface.isInitialized) {
mInterface.close()
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to close VPN interface", e) Log.e(AppConfig.TAG, "StartCore-VPN: Failed to close interface", e)
} }
} }
} }
@@ -14,6 +14,7 @@ import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastSuccess import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsChangeManager import com.v2ray.ang.handler.SettingsChangeManager
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -118,7 +119,7 @@ class SubEditActivity : BaseActivity() {
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm) AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
MmkvManager.removeSubscription(editSubId) SettingsManager.removeSubscriptionWithDefault(editSubId)
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
finish() finish()
} }
@@ -130,7 +131,7 @@ class SubEditActivity : BaseActivity() {
.show() .show()
} else { } else {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
MmkvManager.removeSubscription(editSubId) SettingsManager.removeSubscriptionWithDefault(editSubId)
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
finish() finish()
} }
@@ -145,10 +146,6 @@ class SubEditActivity : BaseActivity() {
del_config = menu.findItem(R.id.del_config) del_config = menu.findItem(R.id.del_config)
save_config = menu.findItem(R.id.save_config) save_config = menu.findItem(R.id.save_config)
if (editSubId.isEmpty() || editSubId == AppConfig.DEFAULT_SUBSCRIPTION_ID) {
del_config?.isVisible = false
}
return super.onCreateOptionsMenu(menu) return super.onCreateOptionsMenu(menu)
} }
@@ -39,7 +39,6 @@ class SubSettingRecyclerAdapter(
holder.itemSubSettingBinding.layoutRemove.setOnClickListener { holder.itemSubSettingBinding.layoutRemove.setOnClickListener {
adapterListener?.onRemove(subId, position) adapterListener?.onRemove(subId, position)
} }
holder.itemSubSettingBinding.layoutRemove.isVisible = subId != AppConfig.DEFAULT_SUBSCRIPTION_ID
holder.itemSubSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked -> holder.itemSubSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked ->
if (!it.isPressed) return@setOnCheckedChangeListener if (!it.isPressed) return@setOnCheckedChangeListener
@@ -5,6 +5,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.util.Log import android.util.Log
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.TestServiceMessage
import com.v2ray.ang.service.V2RayTestService import com.v2ray.ang.service.V2RayTestService
import java.io.Serializable import java.io.Serializable
@@ -37,15 +38,13 @@ object MessageUtil {
* Sends a message to the test service. * Sends a message to the test service.
* *
* @param ctx The context. * @param ctx The context.
* @param what The message identifier. * @param message The test service message containing key, subscriptionId, and serverGuids.
* @param content The message content.
*/ */
fun sendMsg2TestService(ctx: Context, what: Int, content: Serializable) { fun sendMsg2TestService(ctx: Context, message: TestServiceMessage) {
try { try {
val intent = Intent() val intent = Intent()
intent.component = ComponentName(ctx, V2RayTestService::class.java) intent.component = ComponentName(ctx, V2RayTestService::class.java)
intent.putExtra("key", what) intent.putExtra("content", message)
intent.putExtra("content", content)
ctx.startService(intent) ctx.startService(intent)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to send message to test service", e) Log.e(AppConfig.TAG, "Failed to send message to test service", e)
@@ -18,6 +18,7 @@ import com.v2ray.ang.dto.GroupMapItem
import com.v2ray.ang.dto.ServersCache import com.v2ray.ang.dto.ServersCache
import com.v2ray.ang.dto.SubscriptionCache import com.v2ray.ang.dto.SubscriptionCache
import com.v2ray.ang.dto.SubscriptionUpdateResult import com.v2ray.ang.dto.SubscriptionUpdateResult
import com.v2ray.ang.dto.TestServiceMessage
import com.v2ray.ang.extension.serializable import com.v2ray.ang.extension.serializable
import com.v2ray.ang.extension.toastError import com.v2ray.ang.extension.toastError
import com.v2ray.ang.extension.toastSuccess import com.v2ray.ang.extension.toastSuccess
@@ -196,7 +197,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
* Tests the real ping for all servers. * Tests the real ping for all servers.
*/ */
fun testAllRealPing() { fun testAllRealPing() {
MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG_CANCEL, "") MessageUtil.sendMsg2TestService(
getApplication(),
TestServiceMessage(key = AppConfig.MSG_MEASURE_CONFIG_CANCEL)
)
MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList()) MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList())
updateListAction.value = -1 updateListAction.value = -1
@@ -204,7 +208,14 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
if (serversCache.isEmpty()) { if (serversCache.isEmpty()) {
return@launch return@launch
} }
MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG, subscriptionId) MessageUtil.sendMsg2TestService(
getApplication(),
TestServiceMessage(
key = AppConfig.MSG_MEASURE_CONFIG,
subscriptionId = subscriptionId,
serverGuids = if (keywordFilter.isNotEmpty()) serversCache.map { it.guid } else emptyList()
)
)
} }
} }
@@ -241,9 +252,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
} }
val groups = mutableListOf<GroupMapItem>() val groups = mutableListOf<GroupMapItem>()
if (subscriptions.size > 1 if (MmkvManager.decodeSettingsBool(AppConfig.PREF_GROUP_ALL_DISPLAY)) {
&& MmkvManager.decodeSettingsBool(AppConfig.PREF_GROUP_ALL_DISPLAY)
) {
groups.add( groups.add(
GroupMapItem( GroupMapItem(
id = "", id = "",
@@ -21,7 +21,7 @@ class SubscriptionsViewModel : ViewModel() {
fun remove(subId: String): Boolean { fun remove(subId: String): Boolean {
val changed = subscriptions.removeAll { it.guid == subId } val changed = subscriptions.removeAll { it.guid == subId }
if (changed) { if (changed) {
MmkvManager.removeSubscription(subId) SettingsManager.removeSubscriptionWithDefault(subId)
SettingsChangeManager.makeSetupGroupTab() SettingsChangeManager.makeSetupGroupTab()
} }
return changed return changed
@@ -15,7 +15,7 @@ import java.net.HttpURLConnection
class UserAssetViewModel : ViewModel() { class UserAssetViewModel : ViewModel() {
private val assets = mutableListOf<AssetUrlCache>() private val assets = mutableListOf<AssetUrlCache>()
private val builtInGeoFiles = listOf("geosite.dat", "geoip.dat") private val builtInGeoFiles = listOf(AppConfig.GEOSITE_DAT, AppConfig.GEOIP_DAT, AppConfig.GEOIP_ONLY_CN_PRIVATE_DAT)
val itemCount: Int val itemCount: Int
get() = assets.size get() = assets.size
@@ -47,7 +47,18 @@ class UserAssetViewModel : ViewModel() {
) )
) )
} }
return builtInItems + savedAssets // Force update URL for geoip-only-cn-private.dat
return (builtInItems + savedAssets).map { cache ->
if (cache.assetUrl.remarks == AppConfig.GEOIP_ONLY_CN_PRIVATE_DAT) {
cache.copy(
assetUrl = cache.assetUrl.copy(
url = AppConfig.GEOIP_ONLY_CN_PRIVATE_URL
)
)
} else {
cache
}
}
} }
fun downloadGeoFiles(extDir: File, httpPort: Int): GeoDownloadResult { fun downloadGeoFiles(extDir: File, httpPort: Int): GeoDownloadResult {
+1 -1
View File
@@ -1,5 +1,5 @@
[versions] [versions]
agp = "9.0.1" agp = "9.1.0"
desugarJdkLibs = "2.1.5" desugarJdkLibs = "2.1.5"
gradleLicensePlugin = "0.9.8" gradleLicensePlugin = "0.9.8"
kotlin = "2.3.10" kotlin = "2.3.10"