Skip to content
7 changes: 5 additions & 2 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.BIND_VPN_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
Comment on lines 6 to 10
<uses-permission android:name="android.permission.VIBRATE" />

Expand Down Expand Up @@ -45,10 +45,13 @@
<service
android:name="de.unboundtech.defyxvpn.DefyxVpnService"
android:permission="android.permission.BIND_VPN_SERVICE"
android:foregroundServiceType="connectedDevice"
android:foregroundServiceType="specialUse"
android:exported="false"
android:stopWithTask="false"
android:enabled="true">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="vpn_tunnel" />
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
Expand Down
109 changes: 85 additions & 24 deletions android/app/src/main/kotlin/com/defyx/defyx/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import java.io.File
import java.net.*
import java.util.concurrent.atomic.AtomicBoolean
import kotlinx.coroutines.*
import kotlin.coroutines.resume

private const val VPN_REQUEST_CODE = 1000
private const val TAG = "MainActivity"
private const val VPN_OPERATION_TIMEOUT_MS = 30_000L

class MainActivity : FlutterActivity() {
private val CHANNEL = "com.defyx.vpn"
Expand Down Expand Up @@ -83,10 +86,20 @@ class MainActivity : FlutterActivity() {
super.onCreate(savedInstanceState)

val intent = Intent(this, DefyxVpnService::class.java)
DefyxVpnService.setVpnStatusListener { status ->
runOnUiThread { sendVpnStatusToFlutter(status) }
}
grantNotificationPermission()
startService(intent)
}

override fun onDestroy() {
DefyxVpnService.setVpnStatusListener(null)
eventSink = null
clearPendingVpnResult()
super.onDestroy()
}

private suspend fun handleMethodCall(call: MethodCall, result: MethodChannel.Result) {
try {
when (call.method) {
Expand Down Expand Up @@ -130,20 +143,33 @@ class MainActivity : FlutterActivity() {
}
}
private fun connectVpn(result: MethodChannel.Result) {
pendingVpnResult = result

// DefyxVpnService.setVpnStatusListener { status -> sendVpnStatusToFlutter(status) }

if (pendingVpnResult != null) {
result.error("VPN_OPERATION_IN_PROGRESS", "Another VPN permission request is pending", null)
return
}
val vpnIntent = VpnService.prepare(this)
if (vpnIntent != null) {
pendingVpnResult = result
try {
startActivityForResult(vpnIntent, VPN_REQUEST_CODE)
} catch (e: Exception) {
pendingVpnResult = null
result.error("VPN_PERMISSION_ERROR", "Failed to request VPN permission", e.message)
}
} else {
DefyxVpnService.getInstance().startVpn(this)
result.success(true)
DefyxVpnService.getInstance().startVpn(
this,
onConnected = { runOnUiThread { result.success(true) } },
onFailure = { error ->
runOnUiThread {
result.error(
"VPN_FOREGROUND_ERROR",
"Failed to start VPN foreground service",
error.message
)
}
}
)
}
}

Expand Down Expand Up @@ -180,14 +206,59 @@ class MainActivity : FlutterActivity() {
}
}

private fun disconnectVpn(result: MethodChannel.Result) =
try {
DefyxVpnService.getInstance().stopVpn()
sendVpnStatusToFlutter("disconnected")
result.success(true)
} catch (e: Exception) {
result.error("VPN_STOP_ERROR", "Failed to stop VPN", e.message)
private suspend fun disconnectVpn(result: MethodChannel.Result) {
val completed = AtomicBoolean(false)
try {
withTimeout(VPN_OPERATION_TIMEOUT_MS) {
suspendCancellableCoroutine<Unit> { continuation ->
fun complete(block: () -> Unit) {
if (completed.compareAndSet(false, true)) {
block()
continuation.resume(Unit)
}
}
Comment on lines +213 to +219

try {
DefyxVpnService.getInstance().stopVpn(
onComplete = {
runOnUiThread { complete { result.success(true) } }
},
onFailure = { error ->
runOnUiThread {
complete {
result.error(
"VPN_STOP_ERROR",
"Failed to stop VPN",
error.message
)
}
}
}
)
} catch (e: Exception) {
complete { result.error("VPN_STOP_ERROR", "Failed to stop VPN", e.message) }
}
}
}
} catch (e: TimeoutCancellationException) {
completed.set(true)
result.error("VPN_STOP_TIMEOUT", "VPN teardown timed out", e.message)
} catch (e: Exception) {
completed.set(true)
result.error("VPN_STOP_ERROR", "Failed to stop VPN", e.message)
}
}

private fun clearPendingVpnResult() {
pendingVpnResult = null
}

override fun onBackPressed() {
if (pendingVpnResult != null) {
clearPendingVpnResult()
}
super.onBackPressed()
}

private fun getVpnStatus(result: MethodChannel.Result) =
try {
Expand Down Expand Up @@ -273,17 +344,7 @@ class MainActivity : FlutterActivity() {
}

private fun stopVPN(result: MethodChannel.Result) {
CoroutineScope(Dispatchers.IO).launch {
try {
DefyxVpnService.getInstance().disconnectVPN()
result.success(true)
} catch (e: Exception) {
Log.e("Stop VPN", "Stop VPN failed: ${e.message}", e)
withContext(Dispatchers.Main) {
result.error("PING_ERROR", "Failed to Stop VPN", e.localizedMessage)
}
}
}
lifecycleScope.launch { disconnectVpn(result) }
}

private fun getFlag(result: MethodChannel.Result) {
Expand Down
Loading