Android XRay (#840)

* Add XRay module
This commit is contained in:
albexk 2024-06-18 20:46:21 +03:00 committed by GitHub
parent a516d0e757
commit 834b504dff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 637 additions and 151 deletions

View file

@ -34,6 +34,7 @@ import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@ -43,6 +44,8 @@ import org.amnezia.vpn.protocol.getStatus
import org.amnezia.vpn.qt.QtAndroidController
import org.amnezia.vpn.util.Log
import org.amnezia.vpn.util.Prefs
import org.json.JSONException
import org.json.JSONObject
import org.qtproject.qt.android.bindings.QtActivity
private const val TAG = "AmneziaActivity"
@ -59,6 +62,7 @@ class AmneziaActivity : QtActivity() {
private lateinit var mainScope: CoroutineScope
private val qtInitialized = CompletableDeferred<Unit>()
private var vpnProto: VpnProto? = null
private var isWaitingStatus = true
private var isServiceConnected = false
private var isInBoundState = false
@ -141,6 +145,7 @@ class AmneziaActivity : QtActivity() {
override fun onBindingDied(name: ComponentName?) {
Log.w(TAG, "Binding to the ${name?.flattenToString()} unexpectedly died")
doUnbindService()
QtAndroidController.onServiceDisconnected()
doBindService()
}
}
@ -153,15 +158,20 @@ class AmneziaActivity : QtActivity() {
super.onCreate(savedInstanceState)
Log.d(TAG, "Create Amnezia activity: $intent")
mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
val proto = mainScope.async(Dispatchers.IO) {
VpnStateStore.getVpnState().vpnProto
}
vpnServiceMessenger = IpcMessenger(
"VpnService",
onDeadObjectException = {
doUnbindService()
QtAndroidController.onServiceDisconnected()
doBindService()
}
)
registerBroadcastReceivers()
intent?.let(::processIntent)
runBlocking { vpnProto = proto.await() }
}
private fun registerBroadcastReceivers() {
@ -209,13 +219,18 @@ class AmneziaActivity : QtActivity() {
Log.d(TAG, "Start Amnezia activity")
mainScope.launch {
qtInitialized.await()
doBindService()
vpnProto?.let { proto ->
if (AmneziaVpnService.isRunning(applicationContext, proto.processName)) {
doBindService()
}
}
}
}
override fun onStop() {
Log.d(TAG, "Stop Amnezia activity")
doUnbindService()
QtAndroidController.onServiceDisconnected()
super.onStop()
}
@ -269,10 +284,12 @@ class AmneziaActivity : QtActivity() {
@MainThread
private fun doBindService() {
Log.d(TAG, "Bind service")
Intent(this, AmneziaVpnService::class.java).also {
bindService(it, serviceConnection, BIND_ABOVE_CLIENT and BIND_AUTO_CREATE)
vpnProto?.let { proto ->
Intent(this, proto.serviceClass).also {
bindService(it, serviceConnection, BIND_ABOVE_CLIENT and BIND_AUTO_CREATE)
}
isInBoundState = true
}
isInBoundState = true
}
@MainThread
@ -280,7 +297,6 @@ class AmneziaActivity : QtActivity() {
if (isInBoundState) {
Log.d(TAG, "Unbind service")
isWaitingStatus = true
QtAndroidController.onServiceDisconnected()
isServiceConnected = false
vpnServiceMessenger.send(Action.UNREGISTER_CLIENT, activityMessenger)
vpnServiceMessenger.reset()
@ -365,13 +381,31 @@ class AmneziaActivity : QtActivity() {
@MainThread
private fun startVpn(vpnConfig: String) {
if (isServiceConnected) {
connectToVpn(vpnConfig)
} else {
getVpnProto(vpnConfig)?.let { proto ->
Log.d(TAG, "Proto from config: $proto, current proto: $vpnProto")
if (isServiceConnected) {
if (proto == vpnProto) {
connectToVpn(vpnConfig)
return
}
doUnbindService()
}
vpnProto = proto
isWaitingStatus = false
startVpnService(vpnConfig)
startVpnService(vpnConfig, proto)
doBindService()
}
} ?: QtAndroidController.onServiceError()
}
private fun getVpnProto(vpnConfig: String): VpnProto? = try {
require(vpnConfig.isNotBlank()) { "Blank VPN config" }
VpnProto.get(JSONObject(vpnConfig).getString("protocol"))
} catch (e: JSONException) {
Log.e(TAG, "Invalid VPN config json format: ${e.message}")
null
} catch (e: IllegalArgumentException) {
Log.e(TAG, "Protocol not found: ${e.message}")
null
}
private fun connectToVpn(vpnConfig: String) {
@ -383,15 +417,15 @@ class AmneziaActivity : QtActivity() {
}
}
private fun startVpnService(vpnConfig: String) {
Log.d(TAG, "Start VPN service")
Intent(this, AmneziaVpnService::class.java).apply {
private fun startVpnService(vpnConfig: String, proto: VpnProto) {
Log.d(TAG, "Start VPN service: $proto")
Intent(this, proto.serviceClass).apply {
putExtra(MSG_VPN_CONFIG, vpnConfig)
}.also {
try {
ContextCompat.startForegroundService(this, it)
} catch (e: SecurityException) {
Log.e(TAG, "Failed to start AmneziaVpnService: $e")
Log.e(TAG, "Failed to start ${proto.serviceClass.simpleName}: $e")
QtAndroidController.onServiceError()
}
}

View file

@ -39,6 +39,9 @@ class AmneziaTileService : TileService() {
@Volatile
private var isServiceConnected = false
@Volatile
private var vpnProto: VpnProto? = null
private var isInBoundState = false
@Volatile
private var isVpnConfigExists = false
@ -94,16 +97,21 @@ class AmneziaTileService : TileService() {
override fun onStartListening() {
super.onStartListening()
Log.d(TAG, "Start listening")
if (AmneziaVpnService.isRunning(applicationContext)) {
Log.d(TAG, "Vpn service is running")
doBindService()
} else {
Log.d(TAG, "Vpn service is not running")
isServiceConnected = false
updateVpnState(DISCONNECTED)
scope.launch {
Log.d(TAG, "Start listening")
vpnProto = VpnStateStore.getVpnState().vpnProto
vpnProto.also { proto ->
if (proto != null && AmneziaVpnService.isRunning(applicationContext, proto.processName)) {
Log.d(TAG, "Vpn service is running")
doBindService()
} else {
Log.d(TAG, "Vpn service is not running")
isServiceConnected = false
updateVpnState(DISCONNECTED)
}
}
vpnStateListeningJob = launchVpnStateListening()
}
vpnStateListeningJob = launchVpnStateListening()
}
override fun onStopListening() {
@ -124,7 +132,7 @@ class AmneziaTileService : TileService() {
}
private fun onClickInternal() {
if (isVpnConfigExists) {
if (isVpnConfigExists && vpnProto != null) {
Log.d(TAG, "Change VPN state")
if (qsTile.state == Tile.STATE_INACTIVE) {
Log.d(TAG, "Start VPN")
@ -147,10 +155,12 @@ class AmneziaTileService : TileService() {
private fun doBindService() {
Log.d(TAG, "Bind service")
Intent(this, AmneziaVpnService::class.java).also {
bindService(it, serviceConnection, BIND_ABOVE_CLIENT)
vpnProto?.let { proto ->
Intent(this, proto.serviceClass).also {
bindService(it, serviceConnection, BIND_ABOVE_CLIENT)
}
isInBoundState = true
}
isInBoundState = true
}
private fun doUnbindService() {
@ -180,6 +190,7 @@ class AmneziaTileService : TileService() {
if (VpnService.prepare(applicationContext) != null) {
Intent(this, VpnRequestActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
putExtra(EXTRA_PROTOCOL, vpnProto)
}.also {
startActivityAndCollapseCompat(it)
}
@ -189,14 +200,16 @@ class AmneziaTileService : TileService() {
}
private fun startVpnService() {
try {
ContextCompat.startForegroundService(
applicationContext,
Intent(this, AmneziaVpnService::class.java)
)
} catch (e: SecurityException) {
Log.e(TAG, "Failed to start AmneziaVpnService: $e")
}
vpnProto?.let { proto ->
try {
ContextCompat.startForegroundService(
applicationContext,
Intent(this, proto.serviceClass)
)
} catch (e: SecurityException) {
Log.e(TAG, "Failed to start ${proto.serviceClass.simpleName}: $e")
}
} ?: Log.e(TAG, "Failed to start vpn service: vpnProto is null")
}
private fun connectToVpn() = vpnServiceMessenger.send(Action.CONNECT)
@ -220,11 +233,8 @@ class AmneziaTileService : TileService() {
}
}
private fun updateVpnState(state: ProtocolState) {
scope.launch {
VpnStateStore.store { it.copy(protocolState = state) }
}
}
private fun updateVpnState(state: ProtocolState) =
scope.launch { VpnStateStore.store { it.copy(protocolState = state) } }
private fun launchVpnStateListening() =
scope.launch { VpnStateStore.dataFlow().collectLatest(::updateTile) }
@ -232,9 +242,10 @@ class AmneziaTileService : TileService() {
private fun updateTile(vpnState: VpnState) {
Log.d(TAG, "Update tile: $vpnState")
isVpnConfigExists = vpnState.serverName != null
vpnProto = vpnState.vpnProto
val tile = qsTile ?: return
tile.apply {
label = vpnState.serverName ?: DEFAULT_TILE_LABEL
label = (vpnState.serverName ?: DEFAULT_TILE_LABEL) + (vpnProto?.let { " ${it.label}" } ?: "")
when (val protocolState = vpnState.protocolState) {
CONNECTED -> {
state = Tile.STATE_ACTIVE

View file

@ -1,5 +1,6 @@
package org.amnezia.vpn
import android.annotation.SuppressLint
import android.app.ActivityManager
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE
import android.app.NotificationManager
@ -39,7 +40,6 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import org.amnezia.vpn.protocol.BadConfigException
import org.amnezia.vpn.protocol.LoadLibraryException
import org.amnezia.vpn.protocol.Protocol
import org.amnezia.vpn.protocol.ProtocolState.CONNECTED
import org.amnezia.vpn.protocol.ProtocolState.CONNECTING
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
@ -48,11 +48,7 @@ import org.amnezia.vpn.protocol.ProtocolState.RECONNECTING
import org.amnezia.vpn.protocol.ProtocolState.UNKNOWN
import org.amnezia.vpn.protocol.VpnException
import org.amnezia.vpn.protocol.VpnStartException
import org.amnezia.vpn.protocol.awg.Awg
import org.amnezia.vpn.protocol.cloak.Cloak
import org.amnezia.vpn.protocol.openvpn.OpenVpn
import org.amnezia.vpn.protocol.putStatus
import org.amnezia.vpn.protocol.wireguard.Wireguard
import org.amnezia.vpn.util.Log
import org.amnezia.vpn.util.Prefs
import org.amnezia.vpn.util.net.NetworkState
@ -63,6 +59,7 @@ import org.json.JSONObject
private const val TAG = "AmneziaVpnService"
const val ACTION_DISCONNECT = "org.amnezia.vpn.action.disconnect"
const val ACTION_CONNECT = "org.amnezia.vpn.action.connect"
const val MSG_VPN_CONFIG = "VPN_CONFIG"
const val MSG_ERROR = "ERROR"
@ -73,19 +70,18 @@ const val AFTER_PERMISSION_CHECK = "AFTER_PERMISSION_CHECK"
private const val PREFS_CONFIG_KEY = "LAST_CONF"
private const val PREFS_SERVER_NAME = "LAST_SERVER_NAME"
private const val PREFS_SERVER_INDEX = "LAST_SERVER_INDEX"
private const val PROCESS_NAME = "org.amnezia.vpn:amneziaVpnService"
// private const val STATISTICS_SENDING_TIMEOUT = 1000L
private const val TRAFFIC_STATS_UPDATE_TIMEOUT = 1000L
private const val DISCONNECT_TIMEOUT = 5000L
private const val STOP_SERVICE_TIMEOUT = 5000L
class AmneziaVpnService : VpnService() {
@SuppressLint("Registered")
open class AmneziaVpnService : VpnService() {
private lateinit var mainScope: CoroutineScope
private lateinit var connectionScope: CoroutineScope
private var isServiceBound = false
private var protocol: Protocol? = null
private val protocolCache = mutableMapOf<String, Protocol>()
private var vpnProto: VpnProto? = null
private var protocolState = MutableStateFlow(UNKNOWN)
private var serverName: String? = null
private var serverIndex: Int = -1
@ -105,7 +101,7 @@ class AmneziaVpnService : VpnService() {
// private var statisticsSendingJob: Job? = null
private lateinit var networkState: NetworkState
private lateinit var trafficStats: TrafficStats
private var disconnectReceiver: BroadcastReceiver? = null
private var controlReceiver: BroadcastReceiver? = null
private var notificationStateReceiver: BroadcastReceiver? = null
private var screenOnReceiver: BroadcastReceiver? = null
private var screenOffReceiver: BroadcastReceiver? = null
@ -116,7 +112,6 @@ class AmneziaVpnService : VpnService() {
private val connectionExceptionHandler = CoroutineExceptionHandler { _, e ->
protocolState.value = DISCONNECTED
protocol = null
when (e) {
is IllegalArgumentException,
is VpnStartException,
@ -227,7 +222,8 @@ class AmneziaVpnService : VpnService() {
connect(intent?.getStringExtra(MSG_VPN_CONFIG))
}
ServiceCompat.startForeground(
this, NOTIFICATION_ID, serviceNotification.buildNotification(serverName, protocolState.value),
this, NOTIFICATION_ID,
serviceNotification.buildNotification(serverName, vpnProto?.label, protocolState.value),
foregroundServiceTypeCompat
)
return START_REDELIVER_INTENT
@ -292,9 +288,17 @@ class AmneziaVpnService : VpnService() {
private fun registerBroadcastReceivers() {
Log.d(TAG, "Register broadcast receivers")
disconnectReceiver = registerBroadcastReceiver(ACTION_DISCONNECT, ContextCompat.RECEIVER_NOT_EXPORTED) {
Log.d(TAG, "Broadcast request received: $ACTION_DISCONNECT")
disconnect()
controlReceiver = registerBroadcastReceiver(
arrayOf(ACTION_CONNECT, ACTION_DISCONNECT), ContextCompat.RECEIVER_NOT_EXPORTED
) {
it?.action?.let { action ->
Log.d(TAG, "Broadcast request received: $action")
when (action) {
ACTION_CONNECT -> connect()
ACTION_DISCONNECT -> disconnect()
else -> Log.w(TAG, "Unknown action received: $action")
}
}
}
notificationStateReceiver = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
@ -340,10 +344,10 @@ class AmneziaVpnService : VpnService() {
private fun unregisterBroadcastReceivers() {
Log.d(TAG, "Unregister broadcast receivers")
unregisterBroadcastReceiver(disconnectReceiver)
unregisterBroadcastReceiver(controlReceiver)
unregisterBroadcastReceiver(notificationStateReceiver)
unregisterScreenStateBroadcastReceivers()
disconnectReceiver = null
controlReceiver = null
notificationStateReceiver = null
}
@ -356,7 +360,7 @@ class AmneziaVpnService : VpnService() {
protocolState.drop(1).collect { protocolState ->
Log.d(TAG, "Protocol state changed: $protocolState")
serviceNotification.updateNotification(serverName, protocolState)
serviceNotification.updateNotification(serverName, vpnProto?.label, protocolState)
clientMessengers.send {
ServiceEvent.STATUS_CHANGED.packToMessage {
@ -364,7 +368,7 @@ class AmneziaVpnService : VpnService() {
}
}
VpnStateStore.store { VpnState(protocolState, serverName, serverIndex) }
VpnStateStore.store { VpnState(protocolState, serverName, serverIndex, vpnProto) }
when (protocolState) {
CONNECTED -> {
@ -421,7 +425,7 @@ class AmneziaVpnService : VpnService() {
@MainThread
private fun enableNotification() {
registerScreenStateBroadcastReceivers()
serviceNotification.updateNotification(serverName, protocolState.value)
serviceNotification.updateNotification(serverName, vpnProto?.label, protocolState.value)
launchTrafficStatsUpdate()
}
@ -484,8 +488,6 @@ class AmneziaVpnService : VpnService() {
Log.d(TAG, "Start VPN connection")
protocolState.value = CONNECTING
val config = parseConfigToJson(vpnConfig)
saveServerData(config)
if (config == null) {
@ -494,6 +496,16 @@ class AmneziaVpnService : VpnService() {
return
}
try {
vpnProto = VpnProto.get(config.getString("protocol"))
} catch (e: Exception) {
onError("Invalid VPN config: ${e.message}")
protocolState.value = DISCONNECTED
return
}
protocolState.value = CONNECTING
if (!checkPermission()) {
protocolState.value = DISCONNECTED
return
@ -503,8 +515,10 @@ class AmneziaVpnService : VpnService() {
disconnectionJob?.join()
disconnectionJob = null
protocol = getProtocol(config.getString("protocol"))
protocol?.startVpn(config, Builder(), ::protect)
vpnProto?.protocol?.let { protocol ->
protocol.initialize(applicationContext, protocolState, ::onError)
protocol.startVpn(config, Builder(), ::protect)
}
}
}
@ -520,8 +534,8 @@ class AmneziaVpnService : VpnService() {
connectionJob?.join()
connectionJob = null
protocol?.stopVpn()
protocol = null
vpnProto?.protocol?.stopVpn()
try {
withTimeout(DISCONNECT_TIMEOUT) {
// waiting for disconnect state
@ -543,22 +557,10 @@ class AmneziaVpnService : VpnService() {
protocolState.value = RECONNECTING
connectionJob = connectionScope.launch {
protocol?.reconnectVpn(Builder())
vpnProto?.protocol?.reconnectVpn(Builder())
}
}
@MainThread
private fun getProtocol(protocolName: String): Protocol =
protocolCache[protocolName]
?: when (protocolName) {
"wireguard" -> Wireguard()
"awg" -> Awg()
"openvpn" -> OpenVpn()
"cloak" -> Cloak()
else -> throw IllegalArgumentException("Protocol '$protocolName' not found")
}.apply { initialize(applicationContext, protocolState, ::onError) }
.also { protocolCache[protocolName] = it }
/**
* Utils methods
*/
@ -603,6 +605,7 @@ class AmneziaVpnService : VpnService() {
if (prepare(applicationContext) != null) {
Intent(this, VpnRequestActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
putExtra(EXTRA_PROTOCOL, vpnProto)
}.also {
startActivity(it)
}
@ -612,9 +615,9 @@ class AmneziaVpnService : VpnService() {
}
companion object {
fun isRunning(context: Context): Boolean =
fun isRunning(context: Context, processName: String): Boolean =
context.getSystemService<ActivityManager>()!!.runningAppProcesses.any {
it.processName == PROCESS_NAME && it.importance <= IMPORTANCE_FOREGROUND_SERVICE
it.processName == processName && it.importance <= IMPORTANCE_FOREGROUND_SERVICE
}
}
}

View file

@ -0,0 +1,3 @@
package org.amnezia.vpn
class AwgService : AmneziaVpnService()

View file

@ -0,0 +1,3 @@
package org.amnezia.vpn
class OpenVpnService : AmneziaVpnService()

View file

@ -59,14 +59,14 @@ class ServiceNotification(private val context: Context) {
formatSpeedString(rxString, txString)
}
fun buildNotification(serverName: String?, state: ProtocolState): Notification {
fun buildNotification(serverName: String?, protocol: String?, state: ProtocolState): Notification {
val speedString = if (state == CONNECTED) zeroSpeed else null
Log.d(TAG, "Build notification: $serverName, $state")
return notificationBuilder
.setSmallIcon(R.drawable.ic_amnezia_round)
.setContentTitle(serverName ?: "AmneziaVPN")
.setContentTitle((serverName ?: "AmneziaVPN") + (protocol?.let { " $it" } ?: ""))
.setContentText(context.getString(state))
.setSubText(speedString)
.setWhen(System.currentTimeMillis())
@ -96,10 +96,10 @@ class ServiceNotification(private val context: Context) {
}
@SuppressLint("MissingPermission")
fun updateNotification(serverName: String?, state: ProtocolState) {
fun updateNotification(serverName: String?, protocol: String?, state: ProtocolState) {
if (context.isNotificationPermissionGranted()) {
Log.d(TAG, "Update notification: $serverName, $state")
notificationManager.notify(NOTIFICATION_ID, buildNotification(serverName, state))
notificationManager.notify(NOTIFICATION_ID, buildNotification(serverName, protocol, state))
}
}
@ -125,7 +125,7 @@ class ServiceNotification(private val context: Context) {
context,
DISCONNECT_REQUEST_CODE,
Intent(ACTION_DISCONNECT).apply {
setPackage("org.amnezia.vpn")
setPackage(context.packageName)
},
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
@ -135,10 +135,12 @@ class ServiceNotification(private val context: Context) {
DISCONNECTED -> {
Action(
0, context.getString(R.string.connect),
createServicePendingIntent(
PendingIntent.getBroadcast(
context,
CONNECT_REQUEST_CODE,
Intent(context, AmneziaVpnService::class.java),
Intent(ACTION_CONNECT).apply {
setPackage(context.packageName)
},
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
)
@ -148,13 +150,6 @@ class ServiceNotification(private val context: Context) {
}
}
private val createServicePendingIntent: (Context, Int, Intent, Int) -> PendingIntent =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
PendingIntent::getForegroundService
} else {
PendingIntent::getService
}
companion object {
fun createNotificationChannel(context: Context) {
with(NotificationManagerCompat.from(context)) {

View file

@ -0,0 +1,67 @@
package org.amnezia.vpn
import org.amnezia.vpn.protocol.Protocol
import org.amnezia.vpn.protocol.awg.Awg
import org.amnezia.vpn.protocol.cloak.Cloak
import org.amnezia.vpn.protocol.openvpn.OpenVpn
import org.amnezia.vpn.protocol.wireguard.Wireguard
import org.amnezia.vpn.protocol.xray.Xray
enum class VpnProto(
val label: String,
val processName: String,
val serviceClass: Class<out AmneziaVpnService>
) {
WIREGUARD(
"WireGuard",
"org.amnezia.vpn:amneziaAwgService",
AwgService::class.java
) {
override fun createProtocol(): Protocol = Wireguard()
},
AWG(
"AmneziaWG",
"org.amnezia.vpn:amneziaAwgService",
AwgService::class.java
) {
override fun createProtocol(): Protocol = Awg()
},
OPENVPN(
"OpenVPN",
"org.amnezia.vpn:amneziaOpenVpnService",
OpenVpnService::class.java
) {
override fun createProtocol(): Protocol = OpenVpn()
},
CLOAK(
"Cloak",
"org.amnezia.vpn:amneziaOpenVpnService",
OpenVpnService::class.java
) {
override fun createProtocol(): Protocol = Cloak()
},
XRAY(
"XRay",
"org.amnezia.vpn:amneziaXrayService",
XrayService::class.java
) {
override fun createProtocol(): Protocol = Xray()
};
private var _protocol: Protocol? = null
val protocol: Protocol
get() {
if (_protocol == null) _protocol = createProtocol()
return _protocol ?: throw AssertionError("Set to null by another thread")
}
protected abstract fun createProtocol(): Protocol
companion object {
fun get(protocolName: String): VpnProto = VpnProto.valueOf(protocolName.uppercase())
}
}

View file

@ -7,6 +7,7 @@ import android.content.Intent
import android.content.res.Configuration.UI_MODE_NIGHT_MASK
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.net.VpnService
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.widget.Toast
@ -18,9 +19,11 @@ import androidx.core.content.getSystemService
import org.amnezia.vpn.util.Log
private const val TAG = "VpnRequestActivity"
const val EXTRA_PROTOCOL = "PROTOCOL"
class VpnRequestActivity : ComponentActivity() {
private var vpnProto: VpnProto? = null
private var userPresentReceiver: BroadcastReceiver? = null
private val requestLauncher =
registerForActivityResult(StartActivityForResult(), ::checkRequestResult)
@ -28,6 +31,12 @@ class VpnRequestActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(TAG, "Start request activity")
vpnProto = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.extras?.getSerializable(EXTRA_PROTOCOL, VpnProto::class.java)
} else {
@Suppress("DEPRECATION")
intent.extras?.getSerializable(EXTRA_PROTOCOL) as VpnProto
}
val requestIntent = VpnService.prepare(applicationContext)
if (requestIntent != null) {
if (getSystemService<KeyguardManager>()!!.isKeyguardLocked) {
@ -66,10 +75,18 @@ class VpnRequestActivity : ComponentActivity() {
private fun onPermissionGranted() {
Toast.makeText(this, resources.getString(R.string.vpnGranted), Toast.LENGTH_LONG).show()
Intent(applicationContext, AmneziaVpnService::class.java).apply {
putExtra(AFTER_PERMISSION_CHECK, true)
}.also {
ContextCompat.startForegroundService(this, it)
vpnProto?.let { proto ->
Intent(applicationContext, proto.serviceClass).apply {
putExtra(AFTER_PERMISSION_CHECK, true)
}.also {
ContextCompat.startForegroundService(this, it)
}
} ?: run {
Intent(this, AmneziaActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}.also {
startActivity(it)
}
}
}

View file

@ -1,19 +1,22 @@
package org.amnezia.vpn
import android.app.Application
import androidx.datastore.core.CorruptionException
import androidx.datastore.core.MultiProcessDataStoreFactory
import androidx.datastore.core.Serializer
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
import androidx.datastore.dataStoreFile
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.io.OutputStream
import java.io.Serializable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.decodeFromByteArray
import kotlinx.serialization.encodeToByteArray
import kotlinx.serialization.protobuf.ProtoBuf
import org.amnezia.vpn.protocol.ProtocolState
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
import org.amnezia.vpn.util.Log
@ -21,13 +24,14 @@ import org.amnezia.vpn.util.Log
private const val TAG = "VpnState"
private const val STORE_FILE_NAME = "vpnState"
@Serializable
data class VpnState(
val protocolState: ProtocolState,
val serverName: String? = null,
val serverIndex: Int = -1
) : Serializable {
val serverIndex: Int = -1,
val vpnProto: VpnProto? = null
) {
companion object {
private const val serialVersionUID: Long = -1760654961004181606
val defaultState: VpnState = VpnState(DISCONNECTED)
}
}
@ -37,7 +41,11 @@ object VpnStateStore {
private val dataStore = MultiProcessDataStoreFactory.create(
serializer = VpnStateSerializer(),
produceFile = { app.dataStoreFile(STORE_FILE_NAME) }
produceFile = { app.dataStoreFile(STORE_FILE_NAME) },
corruptionHandler = ReplaceFileCorruptionHandler { e ->
Log.e(TAG, "VpnState DataStore corrupted: $e")
VpnState.defaultState
}
)
fun init(app: Application) {
@ -45,36 +53,36 @@ object VpnStateStore {
this.app = app
}
fun dataFlow(): Flow<VpnState> = dataStore.data
fun dataFlow(): Flow<VpnState> = dataStore.data.catch { e ->
Log.e(TAG, "Failed to read VpnState from store: ${e.message}")
emit(VpnState.defaultState)
}
suspend fun getVpnState(): VpnState = dataFlow().firstOrNull() ?: VpnState.defaultState
suspend fun store(f: (vpnState: VpnState) -> VpnState) {
try {
dataStore.updateData(f)
} catch (e : Exception) {
} catch (e: Exception) {
Log.e(TAG, "Failed to store VpnState: $e")
Log.w(TAG, "Remove DataStore file")
app.dataStoreFile(STORE_FILE_NAME).delete()
}
}
}
@OptIn(ExperimentalSerializationApi::class)
private class VpnStateSerializer : Serializer<VpnState> {
override val defaultValue: VpnState = VpnState.defaultState
override suspend fun readFrom(input: InputStream): VpnState {
return withContext(Dispatchers.IO) {
val bios = ByteArrayInputStream(input.readBytes())
ObjectInputStream(bios).use {
it.readObject() as VpnState
}
}
override suspend fun readFrom(input: InputStream): VpnState = try {
ProtoBuf.decodeFromByteArray<VpnState>(input.readBytes())
} catch (e: SerializationException) {
Log.e(TAG, "Failed to deserialize data: $e")
throw CorruptionException("Failed to deserialize data", e)
}
override suspend fun writeTo(t: VpnState, output: OutputStream) {
withContext(Dispatchers.IO) {
val baos = ByteArrayOutputStream()
ObjectOutputStream(baos).use {
it.writeObject(t)
}
output.write(baos.toByteArray())
}
}
@Suppress("BlockingMethodInNonBlockingContext")
override suspend fun writeTo(t: VpnState, output: OutputStream) =
output.write(ProtoBuf.encodeToByteArray(t))
}

View file

@ -0,0 +1,3 @@
package org.amnezia.vpn
class XrayService : AmneziaVpnService()