Skip to content

Commit

Permalink
android: offline api structure (#1918)
Browse files Browse the repository at this point in the history
* add connectivity monitoring
  • Loading branch information
zaneschepke authored Jan 14, 2025
1 parent d213e59 commit 7be9b10
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
package net.nymtech.vpn.backend

import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.os.PowerManager
import android.system.Os
import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import com.getkeepsafe.relinker.ReLinker
import com.getkeepsafe.relinker.ReLinker.LoadListener
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.nymtech.vpn.model.BackendEvent
import net.nymtech.vpn.model.Country
import net.nymtech.vpn.service.network.NetworkConnectivityService
import net.nymtech.vpn.service.network.NetworkStatus
import net.nymtech.vpn.util.Action
import net.nymtech.vpn.util.Constants
import net.nymtech.vpn.util.Constants.LOG_LEVEL
Expand All @@ -29,6 +32,7 @@ import net.nymtech.vpn.util.extensions.startServiceByClass
import nym_vpn_lib.AccountLinks
import nym_vpn_lib.AccountStateSummary
import nym_vpn_lib.AndroidTunProvider
import nym_vpn_lib.ConnectivityObserver
import nym_vpn_lib.GatewayType
import nym_vpn_lib.SystemMessage
import nym_vpn_lib.TunnelEvent
Expand Down Expand Up @@ -75,6 +79,8 @@ class NymBackend private constructor(val context: Context) : Backend, TunnelStat
const val DEFAULT_LOCALE = "en"
}

private val observers: MutableList<ConnectivityObserver> = mutableListOf()

private val initialized = AtomicBoolean(false)

private val ioDispatcher = Dispatchers.IO
Expand All @@ -87,6 +93,9 @@ class NymBackend private constructor(val context: Context) : Backend, TunnelStat
@get:Synchronized @set:Synchronized
private var state: Tunnel.State = Tunnel.State.Down

@get:Synchronized @set:Synchronized
private var networkStatus: NetworkStatus = NetworkStatus.Unknown

override suspend fun init(environment: Tunnel.Environment, credentialMode: Boolean?) {
return withContext(ioDispatcher) {
runCatching {
Expand All @@ -99,6 +108,32 @@ class NymBackend private constructor(val context: Context) : Backend, TunnelStat
}
}

private fun onNetworkStateChange(networkStatus: NetworkStatus) {
this.networkStatus = networkStatus
updateObservers()
}

private fun addObserver(observer: ConnectivityObserver) {
observers.add(observer)
updateObservers()
}

private fun updateObservers() {
val isConnected = when (networkStatus) {
NetworkStatus.Connected -> true
NetworkStatus.Disconnected -> false
NetworkStatus.Unknown -> return
}
Timber.d("Updating observers.. isConnected=$isConnected")
observers.forEach {
it.onNetworkChange(isConnected)
}
}

private fun removeObserver(observer: ConnectivityObserver) {
observers.remove(observer)
}

suspend fun waitForInit() {
runCatching {
val startTime = System.currentTimeMillis()
Expand Down Expand Up @@ -226,16 +261,18 @@ class NymBackend private constructor(val context: Context) : Backend, TunnelStat
if (!initialized.get()) init(tunnel.environment, tunnel.credentialMode)
if (!vpnService.isCompleted) context.startServiceByClass(background, VpnService::class.java)
context.startServiceByClass(background, StateMachineService::class.java)
val service = vpnService.await()
val vpnService = vpnService.await()
val stateMachineService = stateMachineService.await()
val backend = this@NymBackend
service.setOwner(backend)
vpnService.setOwner(backend)
stateMachineService.setOwner(backend)
try {
startVpn(
VpnConfig(
tunnel.entryPoint,
tunnel.exitPoint,
isTwoHop(tunnel.mode),
service,
vpnService,
storagePath,
backend,
tunnel.credentialMode,
Expand Down Expand Up @@ -295,23 +332,35 @@ class NymBackend private constructor(val context: Context) : Backend, TunnelStat
tunnel?.onStateChange(state)
}

internal class StateMachineService : Service() {
internal class StateMachineService : LifecycleService() {

private var owner: NymBackend? = null
private var wakeLock: PowerManager.WakeLock? = null

companion object {
private const val FOREGROUND_ID = 223
const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1024
}

fun setOwner(owner: NymBackend?) {
this.owner = owner
}

override fun onCreate() {
stateMachineService.complete(this)
val notificationManager = NotificationManager.getInstance(this)
ServiceCompat.startForeground(
this,
FOREGROUND_ID,
notificationManager.createStateMachineNotification(),
SYSTEM_EXEMPT_SERVICE_TYPE_ID,
)
lifecycleScope.launch {
NetworkConnectivityService(this@StateMachineService).networkStatus.collect {
Timber.d("New network event: $it")
owner?.onNetworkStateChange(it)
}
}
initWakeLock()
super.onCreate()
}
Expand All @@ -327,12 +376,6 @@ class NymBackend private constructor(val context: Context) : Backend, TunnelStat
super.onDestroy()
}

override fun onBind(p0: Intent?): IBinder? {
return null
}

private val notificationManager = NotificationManager.getInstance(this)

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
stateMachineService.complete(this)
return super.onStartCommand(intent, flags, startId)
Expand All @@ -343,7 +386,7 @@ class NymBackend private constructor(val context: Context) : Backend, TunnelStat
val tag = this.javaClass.name
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
try {
Timber.i("Initiating wakelock forever.. for now..")
Timber.d("Initiating wakelock forever.. for now..")
acquire()
} finally {
release()
Expand Down Expand Up @@ -375,7 +418,6 @@ class NymBackend private constructor(val context: Context) : Backend, TunnelStat
Timber.d("Vpn service destroyed")
vpnService = CompletableDeferred()
stopForeground(STOP_FOREGROUND_REMOVE)
notificationManager.cancel(VPN_NOTIFICATION_ID)
super.onDestroy()
}

Expand Down Expand Up @@ -441,5 +483,13 @@ class NymBackend private constructor(val context: Context) : Backend, TunnelStat
val fd = vpnInterface?.detachFd() ?: return -1
return fd
}

override fun addConnectivityObserver(observer: ConnectivityObserver) {
owner?.addObserver(observer)
}

override fun removeConnectivityObserver(observer: ConnectivityObserver) {
owner?.removeObserver(observer)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package net.nymtech.vpn.service.network

import android.content.Context
import android.net.ConnectivityManager
import android.net.ConnectivityManager.NetworkCallback
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn

class NetworkConnectivityService(context: Context) : NetworkService {

private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

override val networkStatus: Flow<NetworkStatus> = callbackFlow {
val connectivityCallback = object : NetworkCallback() {
override fun onAvailable(network: Network) {
trySend(NetworkStatus.Connected)
}

override fun onUnavailable() {
trySend(NetworkStatus.Disconnected)
}

override fun onLost(network: Network) {
trySend(NetworkStatus.Disconnected)
}

override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
trySend(NetworkStatus.Connected)
}
}

val request = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build()

connectivityManager.registerNetworkCallback(request, connectivityCallback)

awaitClose {
connectivityManager.unregisterNetworkCallback(connectivityCallback)
}
}.distinctUntilChanged().flowOn(Dispatchers.IO)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package net.nymtech.vpn.service.network

import kotlinx.coroutines.flow.Flow

internal interface NetworkService {
val networkStatus: Flow<NetworkStatus>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package net.nymtech.vpn.service.network

sealed class NetworkStatus {
object Unknown : NetworkStatus()
object Connected : NetworkStatus()
object Disconnected : NetworkStatus()
}
10 changes: 5 additions & 5 deletions nym-vpn-android/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
[versions]
accompanist = "0.36.0"
accompanist = "0.37.0"
agp = "8.8.0"
coreSplashscreen = "1.2.0-alpha02"
detektRulesCompose = "1.4.0"
ipaddress = "5.5.1"
jna = "5.15.0"
jna = "5.16.0"
kotlin = "2.1.0"
ksp = "2.1.0-1.0.29"
coreKtx = "1.15.0"
Expand All @@ -22,9 +22,9 @@ datastorePreferences = "1.1.1"
relinker = "1.4.5"
securityCrypto = "1.1.0-alpha06"
timber = "5.0.1"
hiltAndroid = "2.53.1"
kotlinx-serialization-json = "1.7.3"
kotlinxCoroutinesCore = "1.9.0"
hiltAndroid = "2.55"
kotlinx-serialization-json = "1.8.0"
kotlinxCoroutinesCore = "1.10.1"
uiautomator = "2.3.0"
window = "1.3.0"
windowCoreAndroid = "1.3.0"
Expand Down
12 changes: 10 additions & 2 deletions nym-vpn-core/crates/nym-vpn-lib/src/tunnel_provider/android.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
// Copyright 2024 - Nym Technologies SA <[email protected]>
// SPDX-License-Identifier: GPL-3.0-only

use std::{fmt::Debug, os::fd::RawFd};

use super::tunnel_settings::TunnelNetworkSettings;
use crate::platform::error::VpnError;
use std::sync::Arc;
use std::{fmt::Debug, os::fd::RawFd};

#[uniffi::export(with_foreign)]
pub trait ConnectivityObserver: Send + Sync + std::fmt::Debug {
fn on_network_change(&self, is_online: bool);
}

#[uniffi::export(with_foreign)]
pub trait AndroidTunProvider: Send + Sync + Debug {
fn bypass(&self, socket: i32);
fn configure_tunnel(&self, config: TunnelNetworkSettings) -> Result<RawFd, VpnError>;

fn add_connectivity_observer(&self, observer: Arc<dyn ConnectivityObserver>);
fn remove_connectivity_observer(&self, observer: Arc<dyn ConnectivityObserver>);
}

0 comments on commit 7be9b10

Please sign in to comment.