@@ -3,6 +3,7 @@ package com.zaneschepke.wireguardautotunnel.core.service
33import android.app.Notification
44import android.content.Intent
55import android.os.IBinder
6+ import android.text.format.Formatter
67import androidx.core.app.ServiceCompat
78import androidx.lifecycle.LifecycleService
89import androidx.lifecycle.lifecycleScope
@@ -17,6 +18,9 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepos
1718import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
1819import com.zaneschepke.wireguardautotunnel.util.extensions.distinctByKeys
1920import kotlinx.coroutines.CoroutineDispatcher
21+ import kotlinx.coroutines.Job
22+ import kotlinx.coroutines.delay
23+ import kotlinx.coroutines.isActive
2024import kotlinx.coroutines.launch
2125import org.koin.android.ext.android.inject
2226import org.koin.core.qualifier.named
@@ -25,19 +29,18 @@ import timber.log.Timber
2529abstract class BaseTunnelForegroundService : LifecycleService (), TunnelService {
2630
2731 private val notificationManager: NotificationManager by inject()
28-
2932 private val serviceManager: ServiceManager by inject()
30-
3133 private val tunnelManager: TunnelManager by inject()
32-
3334 private val ioDispatcher: CoroutineDispatcher by inject(named(Dispatcher .IO ))
34-
3535 private val settingsRepository: GeneralSettingRepository by inject()
36-
3736 private val tunnelsRepository: TunnelRepository by inject()
3837
3938 protected abstract val fgsType: Int
4039
40+ private var currentSingleTunnelId: Int? = null
41+
42+ private var statsJob: Job ? = null
43+
4144 override fun onBind (intent : Intent ): IBinder {
4245 super .onBind(intent)
4346 return LocalBinder (this )
@@ -55,12 +58,14 @@ abstract class BaseTunnelForegroundService : LifecycleService(), TunnelService {
5558
5659 override fun onStartCommand (intent : Intent ? , flags : Int , startId : Int ): Int {
5760 super .onStartCommand(intent, flags, startId)
61+
5862 ServiceCompat .startForeground(
5963 this ,
6064 NotificationManager .VPN_NOTIFICATION_ID ,
6165 onCreateNotification(),
6266 fgsType,
6367 )
68+
6469 if (
6570 intent == null ||
6671 intent.component == null ||
@@ -79,28 +84,68 @@ abstract class BaseTunnelForegroundService : LifecycleService(), TunnelService {
7984 } else {
8085 start()
8186 }
87+
8288 return START_STICKY
8389 }
8490
8591 override fun start () {
8692 lifecycleScope.launch(ioDispatcher) {
8793 tunnelManager.activeTunnels.distinctByKeys().collect { activeTunnels ->
88- val activeTunConfigs = activeTunnels.keys
94+ val activeTunIds = activeTunnels.keys
8995 val tunnels = tunnelsRepository.getAll()
90- val activeConfigs = tunnels.filter { activeTunConfigs.contains(it.id) }
96+ val activeConfigs = tunnels.filter { activeTunIds.contains(it.id) }
97+
9198 updateServiceNotification(activeConfigs)
99+ restartStatsUpdaterIfNeeded(activeConfigs)
92100 }
93101 }
94102 }
95103
96- // TODO Would be cool to have this include kill switch
104+ private fun restartStatsUpdaterIfNeeded (activeConfigs : List <TunnelConfig >) {
105+ val single = activeConfigs.singleOrNull()
106+
107+ if (single == null ) {
108+ statsJob?.cancel()
109+ statsJob = null
110+ currentSingleTunnelId = null
111+ return
112+ }
113+
114+ if (currentSingleTunnelId == single.id && statsJob?.isActive == true ) return
115+
116+ statsJob?.cancel()
117+ statsJob = null
118+ currentSingleTunnelId = single.id
119+
120+ statsJob =
121+ lifecycleScope.launch(ioDispatcher) {
122+ while (isActive) {
123+ val traffic = readTraffic(single.id)
124+
125+ notificationManager.show(
126+ NotificationManager .VPN_NOTIFICATION_ID ,
127+ createTunnelNotification(single, consumedTraffic = traffic),
128+ )
129+
130+ delay(1000 )
131+ }
132+ }
133+ }
134+
135+ private fun readTraffic (tunnelId : Int ): Pair <Long , Long >? {
136+ val active = tunnelManager.activeTunnels.value[tunnelId] ? : return null
137+ val stats = active.statistics ? : return null
138+ return stats.rx() to stats.tx()
139+ }
140+
97141 private fun updateServiceNotification (activeConfigs : List <TunnelConfig >) {
98142 val notification =
99143 when (activeConfigs.size) {
100144 0 -> onCreateNotification()
101- 1 -> createTunnelNotification(activeConfigs.first())
145+ 1 -> createTunnelNotification(activeConfigs.first(), consumedTraffic = null )
102146 else -> createTunnelsNotification()
103147 }
148+
104149 ServiceCompat .startForeground(
105150 this ,
106151 NotificationManager .VPN_NOTIFICATION_ID ,
@@ -111,21 +156,43 @@ abstract class BaseTunnelForegroundService : LifecycleService(), TunnelService {
111156
112157 override fun stop () {
113158 Timber .d(" Stop called" )
159+ statsJob?.cancel()
160+ statsJob = null
161+ currentSingleTunnelId = null
162+
114163 ServiceCompat .stopForeground(this , ServiceCompat .STOP_FOREGROUND_REMOVE )
115164 stopSelf()
116165 }
117166
118167 override fun onDestroy () {
119168 serviceManager.handleTunnelServiceDestroy()
169+
170+ statsJob?.cancel()
171+ statsJob = null
172+ currentSingleTunnelId = null
173+
120174 ServiceCompat .stopForeground(this , ServiceCompat .STOP_FOREGROUND_REMOVE )
121175 Timber .d(" onDestroy" )
122176 super .onDestroy()
123177 }
124178
125- private fun createTunnelNotification (tunnelConfig : TunnelConfig ): Notification {
179+ private fun createTunnelNotification (
180+ tunnelConfig : TunnelConfig ,
181+ consumedTraffic : Pair <Long , Long >? ,
182+ ): Notification {
183+
184+ val subText =
185+ consumedTraffic?.let { traffic ->
186+ val formattedRx = " ↓ ${formatBytes(traffic.first)} "
187+ val formattedTx = " ↑ ${formatBytes(traffic.second)} "
188+ " $formattedRx $formattedTx "
189+ }
190+
126191 return notificationManager.createNotification(
127192 WireGuardNotification .NotificationChannels .VPN ,
128- title = " ${getString(R .string.tunnel_running)} - ${tunnelConfig.name} " ,
193+ title = tunnelConfig.name,
194+ description = getString(R .string.tunnel_running),
195+ subText = subText,
129196 actions =
130197 listOf (
131198 notificationManager.createNotificationAction(
@@ -160,4 +227,6 @@ abstract class BaseTunnelForegroundService : LifecycleService(), TunnelService {
160227 isGroupSummary = true ,
161228 )
162229 }
230+
231+ private fun formatBytes (bytes : Long ) = Formatter .formatFileSize(this , bytes)
163232}
0 commit comments