From 585176f08d5bbf35680d92dfc1dba9462b6496aa Mon Sep 17 00:00:00 2001 From: zaneschepke Date: Sat, 7 Mar 2026 22:13:38 -0500 Subject: [PATCH] chore: fmt --- .../core/notification/NotificationManager.kt | 4 +- .../notification/WireGuardNotification.kt | 314 +++++++------- .../service/BaseTunnelForegroundService.kt | 405 +++++++++--------- .../currentBackStackEntryAsNavbarState.kt | 4 +- .../ui/screens/tunnels/sort/SortScreen.kt | 1 - .../ui/sideeffect/LocalSideEffect.kt | 2 + 6 files changed, 366 insertions(+), 364 deletions(-) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/NotificationManager.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/NotificationManager.kt index 7471fb4a..57226ced 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/NotificationManager.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/NotificationManager.kt @@ -14,7 +14,7 @@ interface NotificationManager { fun createNotification( channel: NotificationChannels, title: String = "", - subText: String? = null, + subText: String? = null, actions: Collection = emptyList(), description: String = "", showTimestamp: Boolean = true, @@ -28,7 +28,7 @@ interface NotificationManager { fun createNotification( channel: NotificationChannels, title: StringValue, - subText: String? = null, + subText: String? = null, actions: Collection = emptyList(), description: StringValue, showTimestamp: Boolean = true, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/WireGuardNotification.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/WireGuardNotification.kt index c5062511..466fc605 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/WireGuardNotification.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/WireGuardNotification.kt @@ -18,161 +18,161 @@ import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction import com.zaneschepke.wireguardautotunnel.util.StringValue class WireGuardNotification(override val context: Context) : NotificationManager { - - enum class NotificationChannels { - VPN, - AUTO_TUNNEL, - } - - private val notificationManager = NotificationManagerCompat.from(context) - - override fun createNotification( - channel: NotificationChannels, - title: String, - subText: String?, - actions: Collection, - description: String, - showTimestamp: Boolean, - importance: Int, - onGoing: Boolean, - onlyAlertOnce: Boolean, - groupKey: String?, - isGroupSummary: Boolean, - ): Notification { - notificationManager.createNotificationChannel(channel.asChannel(importance)) - return channel - .asBuilder() - .apply { - actions.forEach { addAction(it) } - setContentTitle(title) - setSubText(subText) - setContentIntent( - PendingIntent.getActivity( - context, - 0, - Intent(context, MainActivity::class.java) - .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), - PendingIntent.FLAG_IMMUTABLE, - ) - ) - setContentText(description) - setOnlyAlertOnce(onlyAlertOnce) - setOngoing(onGoing) - setPriority(NotificationCompat.PRIORITY_LOW) - setShowWhen(showTimestamp) - setSmallIcon(R.drawable.ic_notification) - if (groupKey != null) { - setGroup(groupKey) - if (isGroupSummary) { - setGroupSummary(true) - } - } - } - .build() - } - - override fun createNotification( - channel: NotificationChannels, - title: StringValue, - subText: String?, - actions: Collection, - description: StringValue, - showTimestamp: Boolean, - importance: Int, - onGoing: Boolean, - onlyAlertOnce: Boolean, - groupKey: String?, - isGroupSummary: Boolean, - ): Notification { - return createNotification( - channel, - title.asString(context), - subText, - actions, - description.asString(context), - showTimestamp, - importance, - onGoing, - onlyAlertOnce, - ) - } - - override fun createNotificationAction( - notificationAction: NotificationAction, - extraId: Int?, - ): NotificationCompat.Action { - val pendingIntent = - PendingIntent.getBroadcast( - context, - extraId ?: 0, - Intent(context, NotificationActionReceiver::class.java).apply { - action = notificationAction.name - if (extraId != null) putExtra(EXTRA_ID, extraId) - }, - PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, - ) - return NotificationCompat.Action.Builder( - R.drawable.ic_notification, - notificationAction.title(context), - pendingIntent, - ) - .build() - } - - override fun remove(notificationId: Int) { - notificationManager.cancel(notificationId) - } - - override fun show(notificationId: Int, notification: Notification) { - with(notificationManager) { - if ( - ActivityCompat.checkSelfPermission( - context, - Manifest.permission.POST_NOTIFICATIONS, - ) != PackageManager.PERMISSION_GRANTED - ) { - return - } - notify(notificationId, notification) - } - } - - private fun NotificationChannels.asBuilder(): NotificationCompat.Builder { - return when (this) { - NotificationChannels.AUTO_TUNNEL -> { - NotificationCompat.Builder( - context, - context.getString(R.string.auto_tunnel_channel_id), - ) - } - - NotificationChannels.VPN -> { - NotificationCompat.Builder(context, context.getString(R.string.vpn_channel_id)) - } - } - } - - private fun NotificationChannels.asChannel(importance: Int): NotificationChannel { - return when (this) { - NotificationChannels.VPN -> { - NotificationChannel( - context.getString(R.string.vpn_channel_id), - context.getString(R.string.vpn_channel_name), - importance, - ) - .apply { description = context.getString(R.string.vpn_channel_description) } - } - - NotificationChannels.AUTO_TUNNEL -> { - NotificationChannel( - context.getString(R.string.auto_tunnel_channel_id), - context.getString(R.string.auto_tunnel_channel_name), - importance, - ) - .apply { - description = context.getString(R.string.auto_tunnel_channel_description) - } - } - } - } + + enum class NotificationChannels { + VPN, + AUTO_TUNNEL, + } + + private val notificationManager = NotificationManagerCompat.from(context) + + override fun createNotification( + channel: NotificationChannels, + title: String, + subText: String?, + actions: Collection, + description: String, + showTimestamp: Boolean, + importance: Int, + onGoing: Boolean, + onlyAlertOnce: Boolean, + groupKey: String?, + isGroupSummary: Boolean, + ): Notification { + notificationManager.createNotificationChannel(channel.asChannel(importance)) + return channel + .asBuilder() + .apply { + actions.forEach { addAction(it) } + setContentTitle(title) + setSubText(subText) + setContentIntent( + PendingIntent.getActivity( + context, + 0, + Intent(context, MainActivity::class.java) + .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), + PendingIntent.FLAG_IMMUTABLE, + ) + ) + setContentText(description) + setOnlyAlertOnce(onlyAlertOnce) + setOngoing(onGoing) + setPriority(NotificationCompat.PRIORITY_LOW) + setShowWhen(showTimestamp) + setSmallIcon(R.drawable.ic_notification) + if (groupKey != null) { + setGroup(groupKey) + if (isGroupSummary) { + setGroupSummary(true) + } + } + } + .build() + } + + override fun createNotification( + channel: NotificationChannels, + title: StringValue, + subText: String?, + actions: Collection, + description: StringValue, + showTimestamp: Boolean, + importance: Int, + onGoing: Boolean, + onlyAlertOnce: Boolean, + groupKey: String?, + isGroupSummary: Boolean, + ): Notification { + return createNotification( + channel, + title.asString(context), + subText, + actions, + description.asString(context), + showTimestamp, + importance, + onGoing, + onlyAlertOnce, + ) + } + + override fun createNotificationAction( + notificationAction: NotificationAction, + extraId: Int?, + ): NotificationCompat.Action { + val pendingIntent = + PendingIntent.getBroadcast( + context, + extraId ?: 0, + Intent(context, NotificationActionReceiver::class.java).apply { + action = notificationAction.name + if (extraId != null) putExtra(EXTRA_ID, extraId) + }, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, + ) + return NotificationCompat.Action.Builder( + R.drawable.ic_notification, + notificationAction.title(context), + pendingIntent, + ) + .build() + } + + override fun remove(notificationId: Int) { + notificationManager.cancel(notificationId) + } + + override fun show(notificationId: Int, notification: Notification) { + with(notificationManager) { + if ( + ActivityCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS, + ) != PackageManager.PERMISSION_GRANTED + ) { + return + } + notify(notificationId, notification) + } + } + + private fun NotificationChannels.asBuilder(): NotificationCompat.Builder { + return when (this) { + NotificationChannels.AUTO_TUNNEL -> { + NotificationCompat.Builder( + context, + context.getString(R.string.auto_tunnel_channel_id), + ) + } + + NotificationChannels.VPN -> { + NotificationCompat.Builder(context, context.getString(R.string.vpn_channel_id)) + } + } + } + + private fun NotificationChannels.asChannel(importance: Int): NotificationChannel { + return when (this) { + NotificationChannels.VPN -> { + NotificationChannel( + context.getString(R.string.vpn_channel_id), + context.getString(R.string.vpn_channel_name), + importance, + ) + .apply { description = context.getString(R.string.vpn_channel_description) } + } + + NotificationChannels.AUTO_TUNNEL -> { + NotificationChannel( + context.getString(R.string.auto_tunnel_channel_id), + context.getString(R.string.auto_tunnel_channel_name), + importance, + ) + .apply { + description = context.getString(R.string.auto_tunnel_channel_description) + } + } + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/BaseTunnelForegroundService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/BaseTunnelForegroundService.kt index 086a7c08..7cce881f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/BaseTunnelForegroundService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/BaseTunnelForegroundService.kt @@ -25,209 +25,208 @@ import kotlinx.coroutines.launch import org.koin.android.ext.android.inject import org.koin.core.qualifier.named import timber.log.Timber -import java.text.NumberFormat -import java.util.Locale abstract class BaseTunnelForegroundService : LifecycleService(), TunnelService { - - private val notificationManager: NotificationManager by inject() - private val serviceManager: ServiceManager by inject() - private val tunnelManager: TunnelManager by inject() - private val ioDispatcher: CoroutineDispatcher by inject(named(Dispatcher.IO)) - private val settingsRepository: GeneralSettingRepository by inject() - private val tunnelsRepository: TunnelRepository by inject() - - protected abstract val fgsType: Int - - private var currentSingleTunnelId: Int? = null - - private var statsJob: Job? = null - - override fun onBind(intent: Intent): IBinder { - super.onBind(intent) - return LocalBinder(this) - } - - override fun onCreate() { - super.onCreate() - ServiceCompat.startForeground( - this, - NotificationManager.VPN_NOTIFICATION_ID, - onCreateNotification(), - fgsType, - ) - } - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - super.onStartCommand(intent, flags, startId) - - ServiceCompat.startForeground( - this, - NotificationManager.VPN_NOTIFICATION_ID, - onCreateNotification(), - fgsType, - ) - - if ( - intent == null || - intent.component == null || - (intent.component?.packageName != this.packageName) - ) { - Timber.d("Service started by Always-on VPN feature") - lifecycleScope.launch { - val settings = settingsRepository.getGeneralSettings() - if (settings.isAlwaysOnVpnEnabled) { - val tunnel = tunnelsRepository.getDefaultTunnel() - tunnel?.let { tunnelManager.startTunnel(it) } - } else { - Timber.w("Always-on VPN is not enabled in app settings") - } - } - } else { - start() - } - - return START_STICKY - } - - override fun start() { - lifecycleScope.launch(ioDispatcher) { - tunnelManager.activeTunnels.distinctByKeys().collect { activeTunnels -> - val activeTunIds = activeTunnels.keys - val tunnels = tunnelsRepository.getAll() - val activeConfigs = tunnels.filter { activeTunIds.contains(it.id) } - - updateServiceNotification(activeConfigs) - restartStatsUpdaterIfNeeded(activeConfigs) - } - } - } - - private fun restartStatsUpdaterIfNeeded(activeConfigs: List) { - val single = activeConfigs.singleOrNull() - - if (single == null) { - statsJob?.cancel() - statsJob = null - currentSingleTunnelId = null - return - } - - if (currentSingleTunnelId == single.id && statsJob?.isActive == true) return - - statsJob?.cancel() - statsJob = null - currentSingleTunnelId = single.id - - statsJob = lifecycleScope.launch(ioDispatcher) { - while (isActive) { - val traffic = readTraffic(single.id) - - notificationManager - .show( - NotificationManager.VPN_NOTIFICATION_ID, - createTunnelNotification(single, consumedTraffic = traffic), - ) - - delay(1000) - } - } - } - - private fun readTraffic(tunnelId: Int): Pair? { - val active = tunnelManager.activeTunnels.value[tunnelId] ?: return null - val stats = active.statistics ?: return null - return stats.rx() to stats.tx() - } - - private fun updateServiceNotification(activeConfigs: List) { - val notification = - when (activeConfigs.size) { - 0 -> onCreateNotification() - 1 -> createTunnelNotification(activeConfigs.first(), consumedTraffic = null) - else -> createTunnelsNotification() - } - - ServiceCompat.startForeground( - this, - NotificationManager.VPN_NOTIFICATION_ID, - notification, - fgsType, - ) - } - - override fun stop() { - Timber.d("Stop called") - statsJob?.cancel() - statsJob = null - currentSingleTunnelId = null - - ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) - stopSelf() - } - - override fun onDestroy() { - serviceManager.handleTunnelServiceDestroy() - - statsJob?.cancel() - statsJob = null - currentSingleTunnelId = null - - ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) - Timber.d("onDestroy") - super.onDestroy() - } - - private fun createTunnelNotification( - tunnelConfig: TunnelConfig, - consumedTraffic: Pair?, - ): Notification { - - val subText = consumedTraffic?.let { traffic -> - val formattedRx = "↓ ${formatBytes(traffic.first)}" - val formattedTx = "↑ ${formatBytes(traffic.second)}" - "$formattedRx $formattedTx" - } - - return notificationManager.createNotification( - WireGuardNotification.NotificationChannels.VPN, - title = tunnelConfig.name, - description = getString(R.string.tunnel_running), - subText = subText, - actions = listOf( - notificationManager.createNotificationAction( - NotificationAction.TUNNEL_OFF, - tunnelConfig.id, - ) - ), - onGoing = true, - groupKey = NotificationManager.VPN_GROUP_KEY, - isGroupSummary = true, - ) - } - - - private fun createTunnelsNotification(): Notification { - return notificationManager.createNotification( - WireGuardNotification.NotificationChannels.VPN, - title = "${getString(R.string.tunnel_running)} - ${getString(R.string.multiple)}", - actions = - listOf( - notificationManager.createNotificationAction(NotificationAction.TUNNEL_OFF, 0) - ), - groupKey = NotificationManager.VPN_GROUP_KEY, - isGroupSummary = true, - ) - } - - private fun onCreateNotification(): Notification { - return notificationManager.createNotification( - WireGuardNotification.NotificationChannels.VPN, - title = getString(R.string.tunnel_starting), - groupKey = NotificationManager.VPN_GROUP_KEY, - isGroupSummary = true, - ) - } - - private fun formatBytes(bytes: Long) = Formatter.formatFileSize(this, bytes) + + private val notificationManager: NotificationManager by inject() + private val serviceManager: ServiceManager by inject() + private val tunnelManager: TunnelManager by inject() + private val ioDispatcher: CoroutineDispatcher by inject(named(Dispatcher.IO)) + private val settingsRepository: GeneralSettingRepository by inject() + private val tunnelsRepository: TunnelRepository by inject() + + protected abstract val fgsType: Int + + private var currentSingleTunnelId: Int? = null + + private var statsJob: Job? = null + + override fun onBind(intent: Intent): IBinder { + super.onBind(intent) + return LocalBinder(this) + } + + override fun onCreate() { + super.onCreate() + ServiceCompat.startForeground( + this, + NotificationManager.VPN_NOTIFICATION_ID, + onCreateNotification(), + fgsType, + ) + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + super.onStartCommand(intent, flags, startId) + + ServiceCompat.startForeground( + this, + NotificationManager.VPN_NOTIFICATION_ID, + onCreateNotification(), + fgsType, + ) + + if ( + intent == null || + intent.component == null || + (intent.component?.packageName != this.packageName) + ) { + Timber.d("Service started by Always-on VPN feature") + lifecycleScope.launch { + val settings = settingsRepository.getGeneralSettings() + if (settings.isAlwaysOnVpnEnabled) { + val tunnel = tunnelsRepository.getDefaultTunnel() + tunnel?.let { tunnelManager.startTunnel(it) } + } else { + Timber.w("Always-on VPN is not enabled in app settings") + } + } + } else { + start() + } + + return START_STICKY + } + + override fun start() { + lifecycleScope.launch(ioDispatcher) { + tunnelManager.activeTunnels.distinctByKeys().collect { activeTunnels -> + val activeTunIds = activeTunnels.keys + val tunnels = tunnelsRepository.getAll() + val activeConfigs = tunnels.filter { activeTunIds.contains(it.id) } + + updateServiceNotification(activeConfigs) + restartStatsUpdaterIfNeeded(activeConfigs) + } + } + } + + private fun restartStatsUpdaterIfNeeded(activeConfigs: List) { + val single = activeConfigs.singleOrNull() + + if (single == null) { + statsJob?.cancel() + statsJob = null + currentSingleTunnelId = null + return + } + + if (currentSingleTunnelId == single.id && statsJob?.isActive == true) return + + statsJob?.cancel() + statsJob = null + currentSingleTunnelId = single.id + + statsJob = + lifecycleScope.launch(ioDispatcher) { + while (isActive) { + val traffic = readTraffic(single.id) + + notificationManager.show( + NotificationManager.VPN_NOTIFICATION_ID, + createTunnelNotification(single, consumedTraffic = traffic), + ) + + delay(1000) + } + } + } + + private fun readTraffic(tunnelId: Int): Pair? { + val active = tunnelManager.activeTunnels.value[tunnelId] ?: return null + val stats = active.statistics ?: return null + return stats.rx() to stats.tx() + } + + private fun updateServiceNotification(activeConfigs: List) { + val notification = + when (activeConfigs.size) { + 0 -> onCreateNotification() + 1 -> createTunnelNotification(activeConfigs.first(), consumedTraffic = null) + else -> createTunnelsNotification() + } + + ServiceCompat.startForeground( + this, + NotificationManager.VPN_NOTIFICATION_ID, + notification, + fgsType, + ) + } + + override fun stop() { + Timber.d("Stop called") + statsJob?.cancel() + statsJob = null + currentSingleTunnelId = null + + ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) + stopSelf() + } + + override fun onDestroy() { + serviceManager.handleTunnelServiceDestroy() + + statsJob?.cancel() + statsJob = null + currentSingleTunnelId = null + + ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) + Timber.d("onDestroy") + super.onDestroy() + } + + private fun createTunnelNotification( + tunnelConfig: TunnelConfig, + consumedTraffic: Pair?, + ): Notification { + + val subText = + consumedTraffic?.let { traffic -> + val formattedRx = "↓ ${formatBytes(traffic.first)}" + val formattedTx = "↑ ${formatBytes(traffic.second)}" + "$formattedRx $formattedTx" + } + + return notificationManager.createNotification( + WireGuardNotification.NotificationChannels.VPN, + title = tunnelConfig.name, + description = getString(R.string.tunnel_running), + subText = subText, + actions = + listOf( + notificationManager.createNotificationAction( + NotificationAction.TUNNEL_OFF, + tunnelConfig.id, + ) + ), + onGoing = true, + groupKey = NotificationManager.VPN_GROUP_KEY, + isGroupSummary = true, + ) + } + + private fun createTunnelsNotification(): Notification { + return notificationManager.createNotification( + WireGuardNotification.NotificationChannels.VPN, + title = "${getString(R.string.tunnel_running)} - ${getString(R.string.multiple)}", + actions = + listOf( + notificationManager.createNotificationAction(NotificationAction.TUNNEL_OFF, 0) + ), + groupKey = NotificationManager.VPN_GROUP_KEY, + isGroupSummary = true, + ) + } + + private fun onCreateNotification(): Notification { + return notificationManager.createNotification( + WireGuardNotification.NotificationChannels.VPN, + title = getString(R.string.tunnel_starting), + groupKey = NotificationManager.VPN_GROUP_KEY, + isGroupSummary = true, + ) + } + + private fun formatBytes(bytes: Long) = Formatter.formatFileSize(this, bytes) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/components/currentBackStackEntryAsNavbarState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/components/currentBackStackEntryAsNavbarState.kt index c2b16ca2..f7eb298a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/components/currentBackStackEntryAsNavbarState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/components/currentBackStackEntryAsNavbarState.kt @@ -220,7 +220,9 @@ fun currentRouteAsNavbarState( Row { IconButton( onClick = { - sharedViewModel.postSideEffect(LocalSideEffect.SortByLatency) + sharedViewModel.postSideEffect( + LocalSideEffect.SortByLatency + ) } ) { Icon( diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/sort/SortScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/sort/SortScreen.kt index b760b637..a71d7689 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/sort/SortScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunnels/sort/SortScreen.kt @@ -13,7 +13,6 @@ import androidx.compose.material.icons.filled.ArrowUpward import androidx.compose.material.icons.filled.DragHandle import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/sideeffect/LocalSideEffect.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/sideeffect/LocalSideEffect.kt index c0fc0fa5..db4fd2e3 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/sideeffect/LocalSideEffect.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/sideeffect/LocalSideEffect.kt @@ -4,7 +4,9 @@ import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig sealed class LocalSideEffect { data object Sort : LocalSideEffect() + data object SortByLatency : LocalSideEffect() + data class LatencySortFinished( val tunnels: List, val latencies: Map,