.
This commit is contained in:
commit
402d63bd9a
217 files changed
+14697
No files matched your search
@@ -0,0 +1,87 @@
|
||||
package com.v2ray.ang.service
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.util.MessageUtil
|
||||
import com.v2ray.ang.util.Utils
|
||||
import java.lang.ref.SoftReference
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.N)
|
||||
class QSTileService : TileService() {
|
||||
|
||||
fun setState(state: Int) {
|
||||
if (state == Tile.STATE_INACTIVE) {
|
||||
qsTile?.state = Tile.STATE_INACTIVE
|
||||
qsTile?.label = getString(R.string.app_name)
|
||||
qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_stat_name)
|
||||
} else if (state == Tile.STATE_ACTIVE) {
|
||||
qsTile?.state = Tile.STATE_ACTIVE
|
||||
qsTile?.label = V2RayServiceManager.currentConfig?.remarks
|
||||
qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_stat_name)
|
||||
}
|
||||
|
||||
qsTile?.updateTile()
|
||||
}
|
||||
|
||||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
setState(Tile.STATE_INACTIVE)
|
||||
mMsgReceive = ReceiveMessageHandler(this)
|
||||
registerReceiver(mMsgReceive, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY))
|
||||
MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "")
|
||||
}
|
||||
|
||||
override fun onStopListening() {
|
||||
super.onStopListening()
|
||||
|
||||
unregisterReceiver(mMsgReceive)
|
||||
mMsgReceive = null
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
super.onClick()
|
||||
when (qsTile.state) {
|
||||
Tile.STATE_INACTIVE -> {
|
||||
Utils.startVServiceFromToggle(this)
|
||||
}
|
||||
Tile.STATE_ACTIVE -> {
|
||||
Utils.stopVService(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var mMsgReceive: BroadcastReceiver? = null
|
||||
|
||||
private class ReceiveMessageHandler(context: QSTileService) : BroadcastReceiver() {
|
||||
internal var mReference: SoftReference<QSTileService> = SoftReference(context)
|
||||
override fun onReceive(ctx: Context?, intent: Intent?) {
|
||||
val context = mReference.get()
|
||||
when (intent?.getIntExtra("key", 0)) {
|
||||
AppConfig.MSG_STATE_RUNNING -> {
|
||||
context?.setState(Tile.STATE_ACTIVE)
|
||||
}
|
||||
AppConfig.MSG_STATE_NOT_RUNNING -> {
|
||||
context?.setState(Tile.STATE_INACTIVE)
|
||||
}
|
||||
AppConfig.MSG_STATE_START_SUCCESS -> {
|
||||
context?.setState(Tile.STATE_ACTIVE)
|
||||
}
|
||||
AppConfig.MSG_STATE_START_FAILURE -> {
|
||||
context?.setState(Tile.STATE_INACTIVE)
|
||||
}
|
||||
AppConfig.MSG_STATE_STOP_SUCCESS -> {
|
||||
context?.setState(Tile.STATE_INACTIVE)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.v2ray.ang.service
|
||||
|
||||
import android.app.Service
|
||||
|
||||
interface ServiceControl {
|
||||
fun getService(): Service
|
||||
|
||||
fun startService()
|
||||
|
||||
fun stopService()
|
||||
|
||||
fun vpnProtect(socket: Int): Boolean
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.v2ray.ang.service
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.v2ray.ang.util.MyContextWrapper
|
||||
import com.v2ray.ang.util.Utils
|
||||
import java.lang.ref.SoftReference
|
||||
|
||||
class V2RayProxyOnlyService : Service(), ServiceControl {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
V2RayServiceManager.serviceControl = SoftReference(this)
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
V2RayServiceManager.startV2rayPoint()
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
V2RayServiceManager.stopV2rayPoint()
|
||||
}
|
||||
|
||||
override fun getService(): Service {
|
||||
return this
|
||||
}
|
||||
|
||||
override fun startService() {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
override fun stopService() {
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
override fun vpnProtect(socket: Int): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
return null
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
override fun attachBaseContext(newBase: Context?) {
|
||||
val context = newBase?.let {
|
||||
MyContextWrapper.wrap(newBase, Utils.getLocale(newBase))
|
||||
}
|
||||
super.attachBaseContext(context)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
package com.v2ray.ang.service
|
||||
|
||||
import android.app.*
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.dto.ServerConfig
|
||||
import com.v2ray.ang.extension.toSpeedString
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.ui.MainActivity
|
||||
import com.v2ray.ang.util.MessageUtil
|
||||
import com.v2ray.ang.util.MmkvManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import com.v2ray.ang.util.V2rayConfigUtil
|
||||
import go.Seq
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import libv2ray.Libv2ray
|
||||
import libv2ray.V2RayPoint
|
||||
import libv2ray.V2RayVPNServiceSupportsSet
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import java.lang.ref.SoftReference
|
||||
import kotlin.math.min
|
||||
|
||||
object V2RayServiceManager {
|
||||
private const val NOTIFICATION_ID = 1
|
||||
private const val NOTIFICATION_PENDING_INTENT_CONTENT = 0
|
||||
private const val NOTIFICATION_PENDING_INTENT_STOP_V2RAY = 1
|
||||
private const val NOTIFICATION_ICON_THRESHOLD = 3000
|
||||
|
||||
val v2rayPoint: V2RayPoint = Libv2ray.newV2RayPoint(V2RayCallback(), Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1)
|
||||
private val mMsgReceive = ReceiveMessageHandler()
|
||||
private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
|
||||
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
|
||||
|
||||
var serviceControl: SoftReference<ServiceControl>? = null
|
||||
set(value) {
|
||||
field = value
|
||||
Seq.setContext(value?.get()?.getService()?.applicationContext)
|
||||
Libv2ray.initV2Env(Utils.userAssetPath(value?.get()?.getService()))
|
||||
}
|
||||
var currentConfig: ServerConfig? = null
|
||||
|
||||
private var lastQueryTime = 0L
|
||||
private var mBuilder: NotificationCompat.Builder? = null
|
||||
private var mSubscription: Subscription? = null
|
||||
private var mNotificationManager: NotificationManager? = null
|
||||
|
||||
fun startV2Ray(context: Context) {
|
||||
if (settingsStorage?.decodeBool(AppConfig.PREF_PROXY_SHARING) == true) {
|
||||
context.toast(R.string.toast_warning_pref_proxysharing_short)
|
||||
} else {
|
||||
context.toast(R.string.toast_services_start)
|
||||
}
|
||||
val intent = if (settingsStorage?.decodeString(AppConfig.PREF_MODE) ?: "VPN" == "VPN") {
|
||||
Intent(context.applicationContext, V2RayVpnService::class.java)
|
||||
} else {
|
||||
Intent(context.applicationContext, V2RayProxyOnlyService::class.java)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) {
|
||||
context.startForegroundService(intent)
|
||||
} else {
|
||||
context.startService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
private class V2RayCallback : V2RayVPNServiceSupportsSet {
|
||||
override fun shutdown(): Long {
|
||||
val serviceControl = serviceControl?.get() ?: return -1
|
||||
// called by go
|
||||
return try {
|
||||
serviceControl.stopService()
|
||||
0
|
||||
} catch (e: Exception) {
|
||||
Log.d(ANG_PACKAGE, e.toString())
|
||||
-1
|
||||
}
|
||||
}
|
||||
|
||||
override fun prepare(): Long {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun protect(l: Long): Boolean {
|
||||
val serviceControl = serviceControl?.get() ?: return true
|
||||
return serviceControl.vpnProtect(l.toInt())
|
||||
}
|
||||
|
||||
override fun onEmitStatus(l: Long, s: String?): Long {
|
||||
//Logger.d(s)
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun setup(s: String): Long {
|
||||
val serviceControl = serviceControl?.get() ?: return -1
|
||||
//Logger.d(s)
|
||||
return try {
|
||||
serviceControl.startService()
|
||||
lastQueryTime = System.currentTimeMillis()
|
||||
startSpeedNotification()
|
||||
0
|
||||
} catch (e: Exception) {
|
||||
Log.d(ANG_PACKAGE, e.toString())
|
||||
-1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun startV2rayPoint() {
|
||||
val service = serviceControl?.get()?.getService() ?: return
|
||||
val guid = mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER) ?: return
|
||||
val config = MmkvManager.decodeServerConfig(guid) ?: return
|
||||
if (!v2rayPoint.isRunning) {
|
||||
val result = V2rayConfigUtil.getV2rayConfig(service, guid)
|
||||
if (!result.status)
|
||||
return
|
||||
|
||||
try {
|
||||
val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_SERVICE)
|
||||
mFilter.addAction(Intent.ACTION_SCREEN_ON)
|
||||
mFilter.addAction(Intent.ACTION_SCREEN_OFF)
|
||||
mFilter.addAction(Intent.ACTION_USER_PRESENT)
|
||||
service.registerReceiver(mMsgReceive, mFilter)
|
||||
} catch (e: Exception) {
|
||||
Log.d(ANG_PACKAGE, e.toString())
|
||||
}
|
||||
|
||||
v2rayPoint.configureFileContent = result.content
|
||||
v2rayPoint.domainName = config.getV2rayPointDomainAndPort()
|
||||
currentConfig = config
|
||||
|
||||
try {
|
||||
v2rayPoint.runLoop(settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) ?: false)
|
||||
} catch (e: Exception) {
|
||||
Log.d(ANG_PACKAGE, e.toString())
|
||||
}
|
||||
|
||||
if (v2rayPoint.isRunning) {
|
||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
|
||||
showNotification()
|
||||
} else {
|
||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "")
|
||||
cancelNotification()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stopV2rayPoint() {
|
||||
val service = serviceControl?.get()?.getService() ?: return
|
||||
|
||||
if (v2rayPoint.isRunning) {
|
||||
GlobalScope.launch(Dispatchers.Default) {
|
||||
try {
|
||||
v2rayPoint.stopLoop()
|
||||
} catch (e: Exception) {
|
||||
Log.d(ANG_PACKAGE, e.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_STOP_SUCCESS, "")
|
||||
cancelNotification()
|
||||
|
||||
try {
|
||||
service.unregisterReceiver(mMsgReceive)
|
||||
} catch (e: Exception) {
|
||||
Log.d(ANG_PACKAGE, e.toString())
|
||||
}
|
||||
}
|
||||
|
||||
private class ReceiveMessageHandler : BroadcastReceiver() {
|
||||
override fun onReceive(ctx: Context?, intent: Intent?) {
|
||||
val serviceControl = serviceControl?.get() ?: return
|
||||
when (intent?.getIntExtra("key", 0)) {
|
||||
AppConfig.MSG_REGISTER_CLIENT -> {
|
||||
//Logger.e("ReceiveMessageHandler", intent?.getIntExtra("key", 0).toString())
|
||||
if (v2rayPoint.isRunning) {
|
||||
MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_RUNNING, "")
|
||||
} else {
|
||||
MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_NOT_RUNNING, "")
|
||||
}
|
||||
}
|
||||
AppConfig.MSG_UNREGISTER_CLIENT -> {
|
||||
// nothing to do
|
||||
}
|
||||
AppConfig.MSG_STATE_START -> {
|
||||
// nothing to do
|
||||
}
|
||||
AppConfig.MSG_STATE_STOP -> {
|
||||
serviceControl.stopService()
|
||||
}
|
||||
AppConfig.MSG_STATE_RESTART -> {
|
||||
startV2rayPoint()
|
||||
}
|
||||
AppConfig.MSG_MEASURE_DELAY -> {
|
||||
measureV2rayDelay()
|
||||
}
|
||||
}
|
||||
|
||||
when (intent?.action) {
|
||||
Intent.ACTION_SCREEN_OFF -> {
|
||||
Log.d(ANG_PACKAGE, "SCREEN_OFF, stop querying stats")
|
||||
stopSpeedNotification()
|
||||
}
|
||||
Intent.ACTION_SCREEN_ON -> {
|
||||
Log.d(ANG_PACKAGE, "SCREEN_ON, start querying stats")
|
||||
startSpeedNotification()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun measureV2rayDelay() {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val service = serviceControl?.get()?.getService() ?: return@launch
|
||||
var time = -1L
|
||||
var errstr = ""
|
||||
if (v2rayPoint.isRunning) {
|
||||
try {
|
||||
time = v2rayPoint.measureDelay()
|
||||
} catch (e: Exception) {
|
||||
Log.d(ANG_PACKAGE, "measureV2rayDelay: $e")
|
||||
errstr = e.message?.substringAfter("\":") ?: "empty message"
|
||||
}
|
||||
}
|
||||
val result = if (time == -1L) {
|
||||
service.getString(R.string.connection_test_error, errstr)
|
||||
} else {
|
||||
service.getString(R.string.connection_test_available, time)
|
||||
}
|
||||
|
||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, result)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showNotification() {
|
||||
val service = serviceControl?.get()?.getService() ?: return
|
||||
val startMainIntent = Intent(service, MainActivity::class.java)
|
||||
val contentPendingIntent = PendingIntent.getActivity(service,
|
||||
NOTIFICATION_PENDING_INTENT_CONTENT, startMainIntent,
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
} else {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
})
|
||||
|
||||
val stopV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE)
|
||||
stopV2RayIntent.`package` = ANG_PACKAGE
|
||||
stopV2RayIntent.putExtra("key", AppConfig.MSG_STATE_STOP)
|
||||
|
||||
val stopV2RayPendingIntent = PendingIntent.getBroadcast(service,
|
||||
NOTIFICATION_PENDING_INTENT_STOP_V2RAY, stopV2RayIntent,
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
} else {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
})
|
||||
|
||||
val channelId =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
createNotificationChannel()
|
||||
} else {
|
||||
// If earlier version channel ID is not used
|
||||
// https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
|
||||
""
|
||||
}
|
||||
|
||||
mBuilder = NotificationCompat.Builder(service, channelId)
|
||||
.setSmallIcon(R.drawable.ic_stat_name)
|
||||
.setContentTitle(currentConfig?.remarks)
|
||||
.setPriority(NotificationCompat.PRIORITY_MIN)
|
||||
.setOngoing(true)
|
||||
.setShowWhen(false)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setContentIntent(contentPendingIntent)
|
||||
.addAction(R.drawable.ic_close_grey_800_24dp,
|
||||
service.getString(R.string.notification_action_stop_v2ray),
|
||||
stopV2RayPendingIntent)
|
||||
//.build()
|
||||
|
||||
//mBuilder?.setDefaults(NotificationCompat.FLAG_ONLY_ALERT_ONCE) //取消震动,铃声其他都不好使
|
||||
|
||||
service.startForeground(NOTIFICATION_ID, mBuilder?.build())
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private fun createNotificationChannel(): String {
|
||||
val channelId = "RAY_NG_M_CH_ID"
|
||||
val channelName = "V2rayNG Background Service"
|
||||
val chan = NotificationChannel(channelId,
|
||||
channelName, NotificationManager.IMPORTANCE_HIGH)
|
||||
chan.lightColor = Color.DKGRAY
|
||||
chan.importance = NotificationManager.IMPORTANCE_NONE
|
||||
chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
|
||||
getNotificationManager()?.createNotificationChannel(chan)
|
||||
return channelId
|
||||
}
|
||||
|
||||
fun cancelNotification() {
|
||||
val service = serviceControl?.get()?.getService() ?: return
|
||||
service.stopForeground(true)
|
||||
mBuilder = null
|
||||
mSubscription?.unsubscribe()
|
||||
mSubscription = null
|
||||
}
|
||||
|
||||
private fun updateNotification(contentText: String?, proxyTraffic: Long, directTraffic: Long) {
|
||||
if (mBuilder != null) {
|
||||
if (proxyTraffic < NOTIFICATION_ICON_THRESHOLD && directTraffic < NOTIFICATION_ICON_THRESHOLD) {
|
||||
mBuilder?.setSmallIcon(R.drawable.ic_stat_name)
|
||||
} else if (proxyTraffic > directTraffic) {
|
||||
mBuilder?.setSmallIcon(R.drawable.ic_stat_proxy)
|
||||
} else {
|
||||
mBuilder?.setSmallIcon(R.drawable.ic_stat_direct)
|
||||
}
|
||||
mBuilder?.setStyle(NotificationCompat.BigTextStyle().bigText(contentText))
|
||||
mBuilder?.setContentText(contentText) // Emui4.1 need content text even if style is set as BigTextStyle
|
||||
getNotificationManager()?.notify(NOTIFICATION_ID, mBuilder?.build())
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNotificationManager(): NotificationManager? {
|
||||
if (mNotificationManager == null) {
|
||||
val service = serviceControl?.get()?.getService() ?: return null
|
||||
mNotificationManager = service.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
}
|
||||
return mNotificationManager
|
||||
}
|
||||
|
||||
private fun startSpeedNotification() {
|
||||
if (mSubscription == null &&
|
||||
v2rayPoint.isRunning &&
|
||||
settingsStorage?.decodeBool(AppConfig.PREF_SPEED_ENABLED) == true) {
|
||||
var lastZeroSpeed = false
|
||||
val outboundTags = currentConfig?.getAllOutboundTags()
|
||||
outboundTags?.remove(TAG_DIRECT)
|
||||
|
||||
mSubscription = Observable.interval(3, java.util.concurrent.TimeUnit.SECONDS)
|
||||
.subscribe {
|
||||
val queryTime = System.currentTimeMillis()
|
||||
val sinceLastQueryInSeconds = (queryTime - lastQueryTime) / 1000.0
|
||||
var proxyTotal = 0L
|
||||
val text = StringBuilder()
|
||||
outboundTags?.forEach {
|
||||
val up = v2rayPoint.queryStats(it, "uplink")
|
||||
val down = v2rayPoint.queryStats(it, "downlink")
|
||||
if (up + down > 0) {
|
||||
appendSpeedString(text, it, up / sinceLastQueryInSeconds, down / sinceLastQueryInSeconds)
|
||||
proxyTotal += up + down
|
||||
}
|
||||
}
|
||||
val directUplink = v2rayPoint.queryStats(TAG_DIRECT, "uplink")
|
||||
val directDownlink = v2rayPoint.queryStats(TAG_DIRECT, "downlink")
|
||||
val zeroSpeed = (proxyTotal == 0L && directUplink == 0L && directDownlink == 0L)
|
||||
if (!zeroSpeed || !lastZeroSpeed) {
|
||||
if (proxyTotal == 0L) {
|
||||
appendSpeedString(text, outboundTags?.firstOrNull(), 0.0, 0.0)
|
||||
}
|
||||
appendSpeedString(text, TAG_DIRECT, directUplink / sinceLastQueryInSeconds,
|
||||
directDownlink / sinceLastQueryInSeconds)
|
||||
updateNotification(text.toString(), proxyTotal, directDownlink + directUplink)
|
||||
}
|
||||
lastZeroSpeed = zeroSpeed
|
||||
lastQueryTime = queryTime
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun appendSpeedString(text: StringBuilder, name: String?, up: Double, down: Double) {
|
||||
var n = name ?: "no tag"
|
||||
n = n.substring(0, min(n.length, 6))
|
||||
text.append(n)
|
||||
for (i in n.length..6 step 2) {
|
||||
text.append("\t")
|
||||
}
|
||||
text.append("• ${up.toLong().toSpeedString()}↑ ${down.toLong().toSpeedString()}↓\n")
|
||||
}
|
||||
|
||||
private fun stopSpeedNotification() {
|
||||
if (mSubscription != null) {
|
||||
mSubscription?.unsubscribe() //stop queryStats
|
||||
mSubscription = null
|
||||
updateNotification(currentConfig?.remarks, 0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.v2ray.ang.service
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
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_SUCCESS
|
||||
import com.v2ray.ang.util.MessageUtil
|
||||
import com.v2ray.ang.util.SpeedtestUtil
|
||||
import com.v2ray.ang.util.Utils
|
||||
import go.Seq
|
||||
import kotlinx.coroutines.*
|
||||
import libv2ray.Libv2ray
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
class V2RayTestService : Service() {
|
||||
private val realTestScope by lazy { CoroutineScope(Executors.newFixedThreadPool(10).asCoroutineDispatcher()) }
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Seq.setContext(this)
|
||||
Libv2ray.initV2Env(Utils.userAssetPath(this))
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
when (intent?.getIntExtra("key", 0)) {
|
||||
MSG_MEASURE_CONFIG -> {
|
||||
val contentPair = intent.getSerializableExtra("content") as Pair<String, String>
|
||||
realTestScope.launch {
|
||||
val result = SpeedtestUtil.realPing(contentPair.second)
|
||||
MessageUtil.sendMsg2UI(this@V2RayTestService, MSG_MEASURE_CONFIG_SUCCESS, Pair(contentPair.first, result))
|
||||
}
|
||||
}
|
||||
MSG_MEASURE_CONFIG_CANCEL -> {
|
||||
realTestScope.coroutineContext[Job]?.cancelChildren()
|
||||
}
|
||||
}
|
||||
return super.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
package com.v2ray.ang.service
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.*
|
||||
import android.os.Build
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.os.StrictMode
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.dto.ERoutingMode
|
||||
import com.v2ray.ang.util.MmkvManager
|
||||
import com.v2ray.ang.util.MyContextWrapper
|
||||
import com.v2ray.ang.util.Utils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import java.lang.ref.SoftReference
|
||||
|
||||
class V2RayVpnService : VpnService(), ServiceControl {
|
||||
companion object {
|
||||
private const val VPN_MTU = 1500
|
||||
private const val PRIVATE_VLAN4_CLIENT = "26.26.26.1"
|
||||
private const val PRIVATE_VLAN4_ROUTER = "26.26.26.2"
|
||||
private const val PRIVATE_VLAN6_CLIENT = "da26:2626::1"
|
||||
private const val PRIVATE_VLAN6_ROUTER = "da26:2626::2"
|
||||
private const val TUN2SOCKS = "libtun2socks.so"
|
||||
}
|
||||
|
||||
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
|
||||
|
||||
private lateinit var mInterface: ParcelFileDescriptor
|
||||
private var isRunning = false
|
||||
|
||||
//val fd: Int get() = mInterface.fd
|
||||
private lateinit var process: Process
|
||||
|
||||
/**destroy
|
||||
* Unfortunately registerDefaultNetworkCallback is going to return our VPN interface: https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e
|
||||
*
|
||||
* This makes doing a requestNetwork with REQUEST necessary so that we don't get ALL possible networks that
|
||||
* satisfies default network capabilities but only THE default network. Unfortunately we need to have
|
||||
* android.permission.CHANGE_NETWORK_STATE to be able to call requestNetwork.
|
||||
*
|
||||
* Source: https://android.googlesource.com/platform/frameworks/base/+/2df4c7d/services/core/java/com/android/server/ConnectivityService.java#887
|
||||
*/
|
||||
@delegate:RequiresApi(Build.VERSION_CODES.P)
|
||||
private val defaultNetworkRequest by lazy {
|
||||
NetworkRequest.Builder()
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
|
||||
.build()
|
||||
}
|
||||
|
||||
private val connectivity by lazy { getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager }
|
||||
|
||||
@delegate:RequiresApi(Build.VERSION_CODES.P)
|
||||
private val defaultNetworkCallback by lazy {
|
||||
object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
setUnderlyingNetworks(arrayOf(network))
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
|
||||
// it's a good idea to refresh capabilities
|
||||
setUnderlyingNetworks(arrayOf(network))
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
setUnderlyingNetworks(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
val policy = StrictMode.ThreadPolicy.Builder().permitAll().build()
|
||||
StrictMode.setThreadPolicy(policy)
|
||||
V2RayServiceManager.serviceControl = SoftReference(this)
|
||||
}
|
||||
|
||||
override fun onRevoke() {
|
||||
stopV2Ray()
|
||||
}
|
||||
|
||||
// override fun onLowMemory() {
|
||||
// stopV2Ray()
|
||||
// super.onLowMemory()
|
||||
// }
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
V2RayServiceManager.cancelNotification()
|
||||
}
|
||||
|
||||
private fun setup() {
|
||||
val prepare = prepare(this)
|
||||
if (prepare != null) {
|
||||
return
|
||||
}
|
||||
|
||||
// If the old interface has exactly the same parameters, use it!
|
||||
// Configure a builder while parsing the parameters.
|
||||
val builder = Builder()
|
||||
//val enableLocalDns = defaultDPreference.getPrefBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false)
|
||||
|
||||
val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE) ?: ERoutingMode.GLOBAL_PROXY.value
|
||||
|
||||
builder.setMtu(VPN_MTU)
|
||||
builder.addAddress(PRIVATE_VLAN4_CLIENT, 30)
|
||||
//builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
|
||||
if (routingMode == ERoutingMode.BYPASS_LAN.value || routingMode == ERoutingMode.BYPASS_LAN_MAINLAND.value) {
|
||||
resources.getStringArray(R.array.bypass_private_ip_address).forEach {
|
||||
val addr = it.split('/')
|
||||
builder.addRoute(addr[0], addr[1].toInt())
|
||||
}
|
||||
} else {
|
||||
builder.addRoute("0.0.0.0", 0)
|
||||
}
|
||||
|
||||
if (settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) == true) {
|
||||
builder.addAddress(PRIVATE_VLAN6_CLIENT, 126)
|
||||
if (routingMode == ERoutingMode.BYPASS_LAN.value || routingMode == ERoutingMode.BYPASS_LAN_MAINLAND.value) {
|
||||
builder.addRoute("2000::", 3) //currently only 1/8 of total ipV6 is in use
|
||||
} else {
|
||||
builder.addRoute("::", 0)
|
||||
}
|
||||
}
|
||||
|
||||
if (settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
|
||||
builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
|
||||
} else {
|
||||
Utils.getVpnDnsServers()
|
||||
.forEach {
|
||||
if (Utils.isPureIpAddress(it)) {
|
||||
builder.addDnsServer(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
builder.setSession(V2RayServiceManager.currentConfig?.remarks.orEmpty())
|
||||
|
||||
if (settingsStorage?.decodeBool(AppConfig.PREF_PER_APP_PROXY) == true) {
|
||||
val apps = settingsStorage?.decodeStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
|
||||
val bypassApps = settingsStorage?.decodeBool(AppConfig.PREF_BYPASS_APPS) ?: false
|
||||
apps?.forEach {
|
||||
try {
|
||||
if (bypassApps)
|
||||
builder.addDisallowedApplication(it)
|
||||
else
|
||||
builder.addAllowedApplication(it)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
//Logger.d(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close the old interface since the parameters have been changed.
|
||||
try {
|
||||
mInterface.close()
|
||||
} catch (ignored: Exception) {
|
||||
// ignored
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
try {
|
||||
connectivity.requestNetwork(defaultNetworkRequest, defaultNetworkCallback)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
builder.setMetered(false)
|
||||
}
|
||||
|
||||
// Create a new interface using the builder and save the parameters.
|
||||
try {
|
||||
mInterface = builder.establish()!!
|
||||
isRunning = true
|
||||
runTun2socks()
|
||||
} catch (e: Exception) {
|
||||
// non-nullable lateinit var
|
||||
e.printStackTrace()
|
||||
stopV2Ray()
|
||||
}
|
||||
}
|
||||
|
||||
private fun runTun2socks() {
|
||||
val socksPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt())
|
||||
val cmd = arrayListOf(File(applicationContext.applicationInfo.nativeLibraryDir, TUN2SOCKS).absolutePath,
|
||||
"--netif-ipaddr", PRIVATE_VLAN4_ROUTER,
|
||||
"--netif-netmask", "255.255.255.252",
|
||||
"--socks-server-addr", "127.0.0.1:${socksPort}",
|
||||
"--tunmtu", VPN_MTU.toString(),
|
||||
"--sock-path", "sock_path",//File(applicationContext.filesDir, "sock_path").absolutePath,
|
||||
"--enable-udprelay",
|
||||
"--loglevel", "notice")
|
||||
|
||||
if (settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) == true) {
|
||||
cmd.add("--netif-ip6addr")
|
||||
cmd.add(PRIVATE_VLAN6_ROUTER)
|
||||
}
|
||||
if (settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
|
||||
val localDnsPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_LOCAL_DNS_PORT), AppConfig.PORT_LOCAL_DNS.toInt())
|
||||
cmd.add("--dnsgw")
|
||||
cmd.add("127.0.0.1:${localDnsPort}")
|
||||
}
|
||||
Log.d(packageName, cmd.toString())
|
||||
|
||||
try {
|
||||
val proBuilder = ProcessBuilder(cmd)
|
||||
proBuilder.redirectErrorStream(true)
|
||||
process = proBuilder
|
||||
.directory(applicationContext.filesDir)
|
||||
.start()
|
||||
Thread(Runnable {
|
||||
Log.d(packageName,"$TUN2SOCKS check")
|
||||
process.waitFor()
|
||||
Log.d(packageName,"$TUN2SOCKS exited")
|
||||
if (isRunning) {
|
||||
Log.d(packageName,"$TUN2SOCKS restart")
|
||||
runTun2socks()
|
||||
}
|
||||
}).start()
|
||||
Log.d(packageName, process.toString())
|
||||
|
||||
sendFd()
|
||||
} catch (e: Exception) {
|
||||
Log.d(packageName, e.toString())
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendFd() {
|
||||
val fd = mInterface.fileDescriptor
|
||||
val path = File(applicationContext.filesDir, "sock_path").absolutePath
|
||||
Log.d(packageName, path)
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
var tries = 0
|
||||
while (true) try {
|
||||
Thread.sleep(50L shl tries)
|
||||
Log.d(packageName, "sendFd tries: $tries")
|
||||
LocalSocket().use { localSocket ->
|
||||
localSocket.connect(LocalSocketAddress(path, LocalSocketAddress.Namespace.FILESYSTEM))
|
||||
localSocket.setFileDescriptorsForSend(arrayOf(fd))
|
||||
localSocket.outputStream.write(42)
|
||||
}
|
||||
break
|
||||
} catch (e: Exception) {
|
||||
Log.d(packageName, e.toString())
|
||||
if (tries > 5) break
|
||||
tries += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
V2RayServiceManager.startV2rayPoint()
|
||||
return START_STICKY
|
||||
//return super.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
|
||||
private fun stopV2Ray(isForced: Boolean = true) {
|
||||
// val configName = defaultDPreference.getPrefString(PREF_CURR_CONFIG_GUID, "")
|
||||
// val emptyInfo = VpnNetworkInfo()
|
||||
// val info = loadVpnNetworkInfo(configName, emptyInfo)!! + (lastNetworkInfo ?: emptyInfo)
|
||||
// saveVpnNetworkInfo(configName, info)
|
||||
isRunning = false;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
try {
|
||||
connectivity.unregisterNetworkCallback(defaultNetworkCallback)
|
||||
} catch (ignored: Exception) {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Log.d(packageName, "tun2socks destroy")
|
||||
process.destroy()
|
||||
} catch (e: Exception) {
|
||||
Log.d(packageName, e.toString())
|
||||
}
|
||||
|
||||
V2RayServiceManager.stopV2rayPoint()
|
||||
|
||||
if (isForced) {
|
||||
//stopSelf has to be called ahead of mInterface.close(). otherwise v2ray core cannot be stooped
|
||||
//It's strage but true.
|
||||
//This can be verified by putting stopself() behind and call stopLoop and startLoop
|
||||
//in a row for several times. You will find that later created v2ray core report port in use
|
||||
//which means the first v2ray core somehow failed to stop and release the port.
|
||||
stopSelf()
|
||||
|
||||
try {
|
||||
mInterface.close()
|
||||
} catch (ignored: Exception) {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getService(): Service {
|
||||
return this
|
||||
}
|
||||
|
||||
override fun startService() {
|
||||
setup()
|
||||
}
|
||||
|
||||
override fun stopService() {
|
||||
stopV2Ray(true)
|
||||
}
|
||||
|
||||
override fun vpnProtect(socket: Int): Boolean {
|
||||
return protect(socket)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
override fun attachBaseContext(newBase: Context?) {
|
||||
val context = newBase?.let {
|
||||
MyContextWrapper.wrap(newBase, Utils.getLocale(newBase))
|
||||
}
|
||||
super.attachBaseContext(context)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user