From d80ea167f458190cb656b88c8ea124e7681a083c Mon Sep 17 00:00:00 2001 From: zaneschepke Date: Thu, 4 Jun 2026 04:18:38 -0400 Subject: [PATCH] fix: notifications not update, add new notification channels --- .../broadcast/NotificationActionReceiver.kt | 4 + .../core/event/TunnelErrorEvent.kt | 5 - .../core/event/TunnelEventDispatcher.kt | 104 ++++++++-- .../AndroidNotificationService.kt | 103 ++++++--- .../AndroidTunnelNotificationService.kt | 196 ++++++++---------- .../core/notification/NotificationService.kt | 2 + .../notification/TunnelNotificationLine.kt | 9 + .../notification/TunnelNotificationService.kt | 22 +- .../service/autotunnel/AutoTunnelService.kt | 6 +- .../wireguardautotunnel/di/TunnelModule.kt | 9 +- .../domain/enums/BackendMode.kt | 11 - .../domain/enums/NotificationAction.kt | 4 +- .../currentBackStackEntryAsNavbarState.kt | 1 - app/src/main/res/values/strings.xml | 10 + .../tunnel/backend/TunnelBackend.kt | 3 +- .../tunnel/util/BackendException.kt | 1 - 16 files changed, 293 insertions(+), 197 deletions(-) create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/TunnelNotificationLine.kt delete mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/enums/BackendMode.kt diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/broadcast/NotificationActionReceiver.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/broadcast/NotificationActionReceiver.kt index c738c3b0..abd0e3b9 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/broadcast/NotificationActionReceiver.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/broadcast/NotificationActionReceiver.kt @@ -42,6 +42,10 @@ class NotificationActionReceiver : BroadcastReceiver(), KoinComponent { } tunnelCoordinator.stopTunnel(tunnelId) } + + NotificationAction.STOP_ALL.name -> { + tunnelCoordinator.stopActiveTunnels() + } } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/event/TunnelErrorEvent.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/event/TunnelErrorEvent.kt index d219e6b8..c17da23f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/event/TunnelErrorEvent.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/event/TunnelErrorEvent.kt @@ -5,8 +5,6 @@ import com.zaneschepke.tunnel.util.BackendException sealed interface TunnelErrorEvent { data class VpnPermissionDenied(val tunnelId: Int) : TunnelErrorEvent - data class StateConflict(val tunnelId: Int, val message: String) : TunnelErrorEvent - data class InternalFailure(val tunnelId: Int, val message: String) : TunnelErrorEvent data class Socks5PortUnavailable(val tunnelId: Int, val port: Int) : TunnelErrorEvent @@ -16,9 +14,6 @@ sealed interface TunnelErrorEvent { companion object { fun from(throwable: Throwable, id: Int): TunnelErrorEvent { return when (throwable) { - is BackendException.StateConflict -> { - StateConflict(id, throwable.message) - } is BackendException.Unauthorized -> { VpnPermissionDenied(id) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/event/TunnelEventDispatcher.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/event/TunnelEventDispatcher.kt index 11aeb885..f08d9616 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/event/TunnelEventDispatcher.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/event/TunnelEventDispatcher.kt @@ -1,18 +1,28 @@ package com.zaneschepke.wireguardautotunnel.core.event +import android.content.Context import com.zaneschepke.tunnel.event.TunnelEvent +import com.zaneschepke.tunnel.model.BackendMode import com.zaneschepke.tunnel.state.BackendStatus +import com.zaneschepke.wireguardautotunnel.R +import com.zaneschepke.wireguardautotunnel.core.notification.TunnelNotificationLine import com.zaneschepke.wireguardautotunnel.core.notification.TunnelNotificationService +import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository +import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach -class TunnelEventDispatcher(private val notificationManager: TunnelNotificationService) { +class TunnelEventDispatcher( + private val notificationManager: TunnelNotificationService, + private val tunnelRepository: TunnelRepository, + private val context: Context, +) { fun bind( scope: CoroutineScope, @@ -27,15 +37,18 @@ class TunnelEventDispatcher(private val notificationManager: TunnelNotificationS .onEach { event -> when (event) { is TunnelEvent.FallbackToIpv4 -> { - notificationManager.showIpv4Fallback(event.tunnelId) + val name = getTunnelName(event.tunnelId) + notificationManager.showIpv4Fallback(name) } is TunnelEvent.RecoveredToIpv6 -> { - notificationManager.showIpv6Recovery(event.tunnelId) + val name = getTunnelName(event.tunnelId) + notificationManager.showIpv6Recovery(name) } is TunnelEvent.DynamicDnsUpdate -> { - notificationManager.showDynamicDnsUpdate(event.tunnelId) + val name = getTunnelName(event.tunnelId) + notificationManager.showDynamicDnsUpdate(name) } is TunnelEvent.NoRootShellAccess -> { @@ -54,36 +67,85 @@ class TunnelEventDispatcher(private val notificationManager: TunnelNotificationS notificationManager.showVpnRequired() } - is TunnelErrorEvent.StateConflict -> { - notificationManager.showStateConflict(error.tunnelId) - } - is TunnelErrorEvent.InternalFailure -> { notificationManager.showError(error.message) } is TunnelErrorEvent.Socks5PortUnavailable -> { - notificationManager.showSocks5PortUnavailable(error.port) + val name = getTunnelName(error.tunnelId) + notificationManager.showSocks5PortUnavailable(error.port, name) } is TunnelErrorEvent.HttpPortUnavailable -> { - notificationManager.showHttpPortUnavailable(error.port) + val name = getTunnelName(error.tunnelId) + notificationManager.showHttpPortUnavailable(error.port, name) } } } .launchIn(scope) - // update persistent notification for services with the tunnel states - providerStatus - .map { it.activeTunnels } - .distinctUntilChangedBy { map -> - val stateSignature = - map.entries - .sortedBy { it.key } - .map { (_, tunnel) -> tunnel.transportState to tunnel.bootstrapState } - map.size to stateSignature + // vpn + combine(providerStatus.map { it.activeTunnels }, tunnelRepository.userTunnelsFlow) { + activeTunnels, + allTunnels -> + activeTunnels + .mapNotNull { (id, activeTunnel) -> + val mode = activeTunnel.mode ?: return@mapNotNull null + + // Only include VPN / KillSwitchPrimary modes + if ( + mode !is BackendMode.Vpn && mode !is BackendMode.Proxy.KillSwitchPrimary + ) { + return@mapNotNull null + } + + val tunnel = allTunnels.find { it.id == id } ?: return@mapNotNull null + val displayState = DisplayTunnelState.from(activeTunnel) + + TunnelNotificationLine( + id = id, + name = tunnel.name, + displayState = displayState, + ) + } + .associateBy { it.id } + } + .distinctUntilChanged() + .onEach { vpnLines -> notificationManager.updateVpnPersistentNotification(vpnLines) } + .launchIn(scope) + + // proxy + combine(providerStatus.map { it.activeTunnels }, tunnelRepository.userTunnelsFlow) { + activeTunnels, + allTunnels -> + activeTunnels + .mapNotNull { (id, activeTunnel) -> + val mode = activeTunnel.mode ?: return@mapNotNull null + + // Only include Standard Proxy mode + if (mode !is BackendMode.Proxy.Standard) { + return@mapNotNull null + } + + val tunnel = allTunnels.find { it.id == id } ?: return@mapNotNull null + val displayState = DisplayTunnelState.from(activeTunnel) + + TunnelNotificationLine( + id = id, + name = tunnel.name, + displayState = displayState, + ) + } + .associateBy { it.id } + } + .distinctUntilChanged() + .onEach { proxyLines -> + notificationManager.updateProxyPersistentNotification(proxyLines) } - .onEach { status -> notificationManager.updatePersistentNotifications(status) } .launchIn(scope) } + + private suspend fun getTunnelName(tunnelId: Int): String { + return tunnelRepository.getById(tunnelId)?.name ?: context.getString(R.string.unknown) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/AndroidNotificationService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/AndroidNotificationService.kt index baa3e3e2..9b5bf9d4 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/AndroidNotificationService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/AndroidNotificationService.kt @@ -13,6 +13,7 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat.Action import androidx.core.app.NotificationCompat.Builder import androidx.core.app.NotificationManagerCompat +import androidx.core.app.NotificationManagerCompat.IMPORTANCE_HIGH import com.zaneschepke.wireguardautotunnel.MainActivity import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.core.broadcast.NotificationActionReceiver @@ -35,6 +36,7 @@ class AndroidNotificationService(override val context: Context) : NotificationSe onlyAlertOnce: Boolean, groupKey: String?, isGroupSummary: Boolean, + style: NotificationCompat.Style?, ): Notification { notificationManager.createNotificationChannel(channel.asChannel()) return channel @@ -63,6 +65,7 @@ class AndroidNotificationService(override val context: Context) : NotificationSe setGroupSummary(true) } } + style?.let { setStyle(it) } } .build() } @@ -78,6 +81,7 @@ class AndroidNotificationService(override val context: Context) : NotificationSe onlyAlertOnce: Boolean, groupKey: String?, isGroupSummary: Boolean, + style: NotificationCompat.Style?, ): Notification { return createNotification( channel, @@ -90,6 +94,7 @@ class AndroidNotificationService(override val context: Context) : NotificationSe onlyAlertOnce, groupKey, isGroupSummary, + style, ) } @@ -107,7 +112,7 @@ class AndroidNotificationService(override val context: Context) : NotificationSe }, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, ) - return NotificationCompat.Action.Builder( + return Action.Builder( R.drawable.ic_notification, notificationAction.title(context), pendingIntent, @@ -133,50 +138,88 @@ class AndroidNotificationService(override val context: Context) : NotificationSe } } - private fun NotificationChannels.asBuilder(): NotificationCompat.Builder { + private fun NotificationChannels.asBuilder(): Builder { return when (this) { - NotificationChannels.AUTO_TUNNEL -> - Builder(context, context.getString(R.string.auto_tunnel_channel_id)) - NotificationChannels.VPN -> Builder(context, context.getString(R.string.vpn_channel_id)) - - NotificationChannels.PROXY -> + is NotificationChannels.Tunnel.VPN -> + Builder(context, context.getString(R.string.vpn_channel_id)) + is NotificationChannels.Tunnel.Proxy -> Builder(context, context.getString(R.string.proxy_channel_id)) + NotificationChannels.AutoTunnel -> + Builder(context, context.getString(R.string.auto_tunnel_channel_id)) + + NotificationChannels.App -> Builder(context, context.getString(R.string.app_channel_id)) + NotificationChannels.Errors -> + Builder(context, context.getString(R.string.errors_channel_id)) + NotificationChannels.Events -> + Builder(context, context.getString(R.string.events_channel_id)) } } - enum class NotificationChannels(val channelId: Int, val importance: Int) { - VPN(R.string.vpn_channel_id, IMPORTANCE_LOW), - AUTO_TUNNEL(R.string.auto_tunnel_channel_id, IMPORTANCE_LOW), - PROXY(R.string.proxy_channel_id, IMPORTANCE_LOW), + sealed class NotificationChannels(val channelId: Int, val importance: Int) { + sealed class Tunnel(channelId: Int, importance: Int) : + NotificationChannels(channelId, importance) { + + data object VPN : + Tunnel(channelId = R.string.vpn_channel_id, importance = IMPORTANCE_LOW) + + data object Proxy : + Tunnel(channelId = R.string.proxy_channel_id, importance = IMPORTANCE_LOW) + } + + data object AutoTunnel : + NotificationChannels( + channelId = R.string.auto_tunnel_channel_id, + importance = IMPORTANCE_LOW, + ) + + data object Events : + NotificationChannels( + channelId = R.string.events_channel_id, + importance = IMPORTANCE_LOW, + ) + + data object Errors : + NotificationChannels( + channelId = R.string.errors_channel_id, + importance = IMPORTANCE_HIGH, + ) + + data object App : + NotificationChannels(channelId = R.string.app_channel_id, importance = IMPORTANCE_LOW) + + companion object { + val all: List = + listOf(Errors, Events, App, Tunnel.VPN, Tunnel.Proxy, AutoTunnel) + } } fun NotificationChannels.asChannel(): NotificationChannel { + val (nameResId, descriptionResId) = + when (this) { + is NotificationChannels.Tunnel.VPN -> + R.string.vpn to R.string.vpn_channel_description + is NotificationChannels.Tunnel.Proxy -> + R.string.proxy to R.string.proxy_channel_description + NotificationChannels.AutoTunnel -> + R.string.auto_tunnel to R.string.auto_tunnel_channel_description + + NotificationChannels.Errors -> + R.string.errors to R.string.errors_channel_description + NotificationChannels.Events -> + R.string.events to R.string.events_channel_description + NotificationChannels.App -> R.string.app to R.string.app_channel_description + } + return NotificationChannel( context.getString(channelId), - context.getString( - when (this) { - NotificationChannels.VPN -> R.string.vpn - NotificationChannels.AUTO_TUNNEL -> R.string.auto_tunnel - NotificationChannels.PROXY -> R.string.proxy - } - ), + context.getString(nameResId), importance, ) - .apply { - description = - context.getString( - when (this@asChannel) { - NotificationChannels.VPN -> R.string.vpn_channel_description - NotificationChannels.AUTO_TUNNEL -> - R.string.auto_tunnel_channel_description - NotificationChannels.PROXY -> R.string.proxy_channel_description - } - ) - } + .apply { description = context.getString(descriptionResId) } } override fun createAllChannels() { - NotificationChannels.entries.forEach { channel -> + NotificationChannels.all.forEach { channel -> notificationManager.createNotificationChannel(channel.asChannel()) } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/AndroidTunnelNotificationService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/AndroidTunnelNotificationService.kt index 9de46f0a..457f909a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/AndroidTunnelNotificationService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/AndroidTunnelNotificationService.kt @@ -1,7 +1,6 @@ package com.zaneschepke.wireguardautotunnel.core.notification -import com.zaneschepke.tunnel.model.BackendMode -import com.zaneschepke.tunnel.state.ActiveTunnel +import androidx.core.app.NotificationCompat import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService.NotificationChannels import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.PROXY_GROUP_KEY @@ -11,164 +10,154 @@ import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.VPN_GROUP_KEY import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.VPN_NOTIFICATION_ID import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction -import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository -import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState -class AndroidTunnelNotificationService( - private val notificationService: NotificationService, - private val tunnelRepository: TunnelRepository, -) : TunnelNotificationService { +class AndroidTunnelNotificationService(private val notificationService: NotificationService) : + TunnelNotificationService { - override suspend fun updatePersistentNotifications(activeTunnels: Map) { + private val context = notificationService.context - val vpnTunnels = activeTunnels.filterValues { it.mode is BackendMode.Vpn } - - val proxyTunnels = activeTunnels.filterValues { it.mode is BackendMode.Proxy } - - updateGroupNotification( - tunnels = vpnTunnels, - notificationId = VPN_NOTIFICATION_ID, - channel = NotificationChannels.VPN, - groupKey = VPN_GROUP_KEY, - ) - - updateGroupNotification( - tunnels = proxyTunnels, - notificationId = PROXY_NOTIFICATION_ID, - channel = NotificationChannels.PROXY, - groupKey = PROXY_GROUP_KEY, - ) - } - - private suspend fun updateGroupNotification( - tunnels: Map, + private fun updateGroupNotification( + tunnelNotificationLines: Map, notificationId: Int, - channel: NotificationChannels, + channel: NotificationChannels.Tunnel, groupKey: String, ) { - - if (tunnels.isEmpty()) { + if (tunnelNotificationLines.isEmpty()) { notificationService.remove(notificationId) return } val context = notificationService.context - val lines = tunnels.mapNotNull { (id, activeTunnel) -> - val tunnel = tunnelRepository.getById(id) ?: return@mapNotNull null - val display = DisplayTunnelState.from(activeTunnel) - + val formattedLines = tunnelNotificationLines.values.map { line -> + val status = line.displayState.asLocalizedString(context) context.getString( R.string.notification_tunnel_status_format, - tunnel.name, - display.asLocalizedString(context), + line.name, + status, ) } - val description = lines.joinToString("\n") + val description = formattedLines.joinToString("\n") - val stopActions = - tunnels.keys.map { + val actions = if (tunnelNotificationLines.size == 1) { + val tunnelId = tunnelNotificationLines.keys.first() + listOf( notificationService.createNotificationAction( notificationAction = NotificationAction.TUNNEL_OFF, - extraId = it, + extraId = tunnelId, ) - } - - val title = - when (channel) { - NotificationChannels.VPN -> context.getString(R.string.vpn) - - NotificationChannels.PROXY -> context.getString(R.string.proxy) - - NotificationChannels.AUTO_TUNNEL -> context.getString(R.string.auto_tunnel) - } - - val notification = - notificationService.createNotification( - channel = channel, - title = title, - description = description, - actions = stopActions, - onGoing = true, - onlyAlertOnce = true, - groupKey = groupKey, ) + } else { + listOf( + notificationService.createNotificationAction( + notificationAction = NotificationAction.STOP_ALL, + extraId = null, + ) + ) + } + + val title = when (channel) { + is NotificationChannels.Tunnel.VPN -> context.getString(R.string.vpn) + is NotificationChannels.Tunnel.Proxy -> context.getString(R.string.proxy) + } + + val style = if (tunnelNotificationLines.size > 1) { + NotificationCompat.InboxStyle() + .setBigContentTitle(title) + .setSummaryText("${tunnelNotificationLines.size} ${context.getString(R.string.tunnels).lowercase()}") + .also { inboxStyle -> + formattedLines.forEach { inboxStyle.addLine(it) } + } + } else { + null + } + + val notification = notificationService.createNotification( + channel = channel, + title = title, + description = description, + actions = actions, + onGoing = true, + onlyAlertOnce = true, + groupKey = groupKey, + style = style, + ) notificationService.show(notificationId, notification) } - override suspend fun showIpv4Fallback(tunnelId: Int) { + override fun updateProxyPersistentNotification( + tunnelNotificationLines: Map + ) { + updateGroupNotification( + tunnelNotificationLines = tunnelNotificationLines, + notificationId = PROXY_NOTIFICATION_ID, + channel = NotificationChannels.Tunnel.Proxy, + groupKey = PROXY_GROUP_KEY, + ) + } - val context = notificationService.context - val name = tunnelName(tunnelId) + override fun updateVpnPersistentNotification( + tunnelNotificationLines: Map + ) { + updateGroupNotification( + tunnelNotificationLines = tunnelNotificationLines, + notificationId = VPN_NOTIFICATION_ID, + channel = NotificationChannels.Tunnel.VPN, + groupKey = VPN_GROUP_KEY, + ) + } - showMessage( + override fun showIpv4Fallback(tunnelName: String) { + showEvent( title = context.getString(R.string.ipv4_fallback), - message = context.getString(R.string.notification_ipv4_fallback_message, name), + message = context.getString(R.string.notification_ipv4_fallback_message, tunnelName), ) } - override suspend fun showIpv6Recovery(tunnelId: Int) { - - val context = notificationService.context - val name = tunnelName(tunnelId) - - showMessage( + override fun showIpv6Recovery(tunnelName: String) { + showEvent( title = context.getString(R.string.ipv6_recovery), - message = context.getString(R.string.notification_ipv6_recovery_message, name), + message = context.getString(R.string.notification_ipv6_recovery_message, tunnelName), ) } - override suspend fun showDynamicDnsUpdate(tunnelId: Int) { - - val context = notificationService.context - val name = tunnelName(tunnelId) - - showMessage( + override fun showDynamicDnsUpdate(tunnelName: String) { + showEvent( title = context.getString(R.string.dynamic_dns_update), - message = context.getString(R.string.notification_dynamic_dns_message, name), + message = context.getString(R.string.notification_dynamic_dns_message, tunnelName), ) } - override suspend fun showVpnRequired() { - + override fun showVpnRequired() { showError(notificationService.context.getString(R.string.vpn_permission_required)) } - override suspend fun showStateConflict(tunnelId: Int) { - - val context = notificationService.context - val name = tunnelName(tunnelId) - - showError(context.getString(R.string.notification_tunnel_already_running, name)) - } - - override suspend fun showRootShellAccess() { + override fun showRootShellAccess() { // TODO could improve with fix action val context = notificationService.context showError(context.getString(R.string.error_root_denied)) } - override suspend fun showSocks5PortUnavailable(port: Int) { + override fun showSocks5PortUnavailable(port: Int, tunnelName: String) { val context = notificationService.context val message = context.getString(R.string.error_socks5_port_unavailable, port) showError(message) } - override suspend fun showHttpPortUnavailable(port: Int) { + override fun showHttpPortUnavailable(port: Int, tunnelName: String) { val context = notificationService.context val message = context.getString(R.string.error_http_port_unavailable, port) showError(message) } - override suspend fun showError(message: String) { - + override fun showError(message: String) { val notification = notificationService.createNotification( - channel = NotificationChannels.VPN, + channel = NotificationChannels.Errors, title = notificationService.context.getString(R.string.error), description = message, onGoing = false, @@ -179,11 +168,11 @@ class AndroidTunnelNotificationService( notificationService.show(TUNNEL_ERROR_NOTIFICATION_ID, notification) } - private fun showMessage(title: String, message: String) { + private fun showEvent(title: String, message: String) { val notification = notificationService.createNotification( - channel = NotificationChannels.VPN, + channel = NotificationChannels.Events, title = title, description = message, onGoing = false, @@ -193,11 +182,4 @@ class AndroidTunnelNotificationService( notificationService.show(TUNNEL_MESSAGES_NOTIFICATION_ID, notification) } - - private suspend fun tunnelName(id: Int): String { - - val context = notificationService.context - - return tunnelRepository.getById(id)?.name ?: context.getString(R.string.unknown, id) - } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/NotificationService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/NotificationService.kt index aec7f5ee..c9113ccd 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/NotificationService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/NotificationService.kt @@ -21,6 +21,7 @@ interface NotificationService { onlyAlertOnce: Boolean = true, groupKey: String? = null, isGroupSummary: Boolean = false, + style: NotificationCompat.Style? = null, ): Notification fun createNotification( @@ -34,6 +35,7 @@ interface NotificationService { onlyAlertOnce: Boolean = true, groupKey: String? = null, isGroupSummary: Boolean = false, + style: NotificationCompat.Style? = null, ): Notification fun createAllChannels() diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/TunnelNotificationLine.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/TunnelNotificationLine.kt new file mode 100644 index 00000000..ae4ab2e8 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/TunnelNotificationLine.kt @@ -0,0 +1,9 @@ +package com.zaneschepke.wireguardautotunnel.core.notification + +import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState + +data class TunnelNotificationLine( + val id: Int, + val name: String, + val displayState: DisplayTunnelState, +) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/TunnelNotificationService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/TunnelNotificationService.kt index 042d2da1..69389329 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/TunnelNotificationService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/notification/TunnelNotificationService.kt @@ -1,26 +1,24 @@ package com.zaneschepke.wireguardautotunnel.core.notification -import com.zaneschepke.tunnel.state.ActiveTunnel - interface TunnelNotificationService { - suspend fun updatePersistentNotifications(activeTunnels: Map) + fun updateProxyPersistentNotification(tunnelNotificationLines: Map) - suspend fun showIpv4Fallback(tunnelId: Int) + fun updateVpnPersistentNotification(tunnelNotificationLines: Map) - suspend fun showIpv6Recovery(tunnelId: Int) + fun showIpv4Fallback(tunnelName: String) - suspend fun showDynamicDnsUpdate(tunnelId: Int) + fun showIpv6Recovery(tunnelName: String) - suspend fun showVpnRequired() + fun showDynamicDnsUpdate(tunnelName: String) - suspend fun showStateConflict(tunnelId: Int) + fun showVpnRequired() - suspend fun showSocks5PortUnavailable(port: Int) + fun showSocks5PortUnavailable(port: Int, tunnelName: String) - suspend fun showHttpPortUnavailable(port: Int) + fun showHttpPortUnavailable(port: Int, tunnelName: String) - suspend fun showRootShellAccess() + fun showRootShellAccess() - suspend fun showError(message: String) + fun showError(message: String) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/autotunnel/AutoTunnelService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/autotunnel/AutoTunnelService.kt index ae7719c9..3d7da2b8 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/autotunnel/AutoTunnelService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/autotunnel/AutoTunnelService.kt @@ -181,7 +181,7 @@ class AutoTunnelService : LifecycleService() { ) { val notification = notificationService.createNotification( - AndroidNotificationService.NotificationChannels.AUTO_TUNNEL, + AndroidNotificationService.NotificationChannels.AutoTunnel, title = getString(R.string.auto_tunnel_title), description = description, actions = @@ -295,7 +295,7 @@ class AutoTunnelService : LifecycleService() { if (!state.locationPermissionsEnabled) { val notification = notificationService.createNotification( - AndroidNotificationService.NotificationChannels.AUTO_TUNNEL, + AndroidNotificationService.NotificationChannels.AutoTunnel, title = getString(R.string.warning), description = getString(R.string.location_permissions_missing), ) @@ -313,7 +313,7 @@ class AutoTunnelService : LifecycleService() { if (!state.locationServicesEnabled) { val notification = notificationService.createNotification( - AndroidNotificationService.NotificationChannels.AUTO_TUNNEL, + AndroidNotificationService.NotificationChannels.AutoTunnel, title = getString(R.string.warning), description = getString(R.string.location_services_not_detected), ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/TunnelModule.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/TunnelModule.kt index 5fd9bd94..db89ced3 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/TunnelModule.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/TunnelModule.kt @@ -21,6 +21,7 @@ import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelBackendProvider import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository import com.zaneschepke.wireguardautotunnel.util.extensions.to +import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.distinctUntilChangedBy @@ -34,7 +35,7 @@ import org.koin.dsl.module import timber.log.Timber val tunnelBackendProviderModule = module { - single { AndroidTunnelNotificationService(get(), get()) } + single { AndroidTunnelNotificationService(get()) } singleOf(::TunnelEventDispatcher) single { @@ -44,7 +45,7 @@ val tunnelBackendProviderModule = module { override val vpnInitNotification: Notification get() = notificationService.createNotification( - channel = NotificationChannels.VPN, + channel = NotificationChannels.Tunnel.VPN, title = context.getString(R.string.initializing), onGoing = true, groupKey = VPN_GROUP_KEY, @@ -53,7 +54,7 @@ val tunnelBackendProviderModule = module { override val proxyInitNotification: Notification get() = notificationService.createNotification( - channel = NotificationChannels.PROXY, + channel = NotificationChannels.Tunnel.Proxy, title = context.getString(R.string.initializing), onGoing = true, groupKey = PROXY_GROUP_KEY, @@ -84,7 +85,7 @@ val tunnelBackendProviderModule = module { object : AndroidNetworkMonitor.ConfigurationListener { override suspend fun runRootShellCommand(cmd: String): String? { return try { - withTimeout(3_000) { + withTimeout(3_000.milliseconds) { withContext(Dispatchers.IO) { val result = RootShell.run(cmd) result.output diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/enums/BackendMode.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/enums/BackendMode.kt deleted file mode 100644 index 1b1a9306..00000000 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/enums/BackendMode.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.zaneschepke.wireguardautotunnel.domain.enums - -sealed class BackendMode { - data object Inactive : BackendMode() - - data class KillSwitch( - val allowedIps: Set, - val isMetered: Boolean, - val dualStack: Boolean, - ) : BackendMode() -} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/enums/NotificationAction.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/enums/NotificationAction.kt index 3a8a9c4d..5d0215d5 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/enums/NotificationAction.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/enums/NotificationAction.kt @@ -5,12 +5,14 @@ import com.zaneschepke.wireguardautotunnel.R enum class NotificationAction { TUNNEL_OFF, - AUTO_TUNNEL_OFF; + AUTO_TUNNEL_OFF, + STOP_ALL; fun title(context: Context): String { return when (this) { TUNNEL_OFF -> context.getString(R.string.stop) AUTO_TUNNEL_OFF -> context.getString(R.string.stop) + STOP_ALL -> context.getString(R.string.stop_all) } } } 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 2d9ef93f..72fa929f 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 @@ -14,7 +14,6 @@ import androidx.compose.material.icons.outlined.RemoveRedEye import androidx.compose.material.icons.rounded.Add import androidx.compose.material.icons.rounded.Delete import androidx.compose.material.icons.rounded.Download -import androidx.compose.material.icons.rounded.Edit import androidx.compose.material.icons.rounded.Menu import androidx.compose.material.icons.rounded.NetworkCheck import androidx.compose.material.icons.rounded.Save diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 92e66c44..3fcb37e0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -570,4 +570,14 @@ SOCKS5 port %1$d is already in use.\nPlease choose a different port. HTTP listener port %1$d is already in use.\nPlease choose a different port. + Events Channel + Errors Channel + Errors + Events + A channel for application and tunnel errors + A channel for app events, like automation event + App Channel + App + A channel for general application notifications, like version updates + Stop all diff --git a/tunnel/src/main/java/com/zaneschepke/tunnel/backend/TunnelBackend.kt b/tunnel/src/main/java/com/zaneschepke/tunnel/backend/TunnelBackend.kt index 755c74ef..bd8bf780 100644 --- a/tunnel/src/main/java/com/zaneschepke/tunnel/backend/TunnelBackend.kt +++ b/tunnel/src/main/java/com/zaneschepke/tunnel/backend/TunnelBackend.kt @@ -154,9 +154,10 @@ class TunnelBackend( } override suspend fun stop(id: Int): Result = runCatching { + // TODO need a clean localized message for this passed by provider val runtime = actor.state.value.byTunnelId[id] - ?: throw BackendException.StateConflict( + ?: throw BackendException.InternalError( "Tunnel $id is not active or no longer exists" ) diff --git a/tunnel/src/main/java/com/zaneschepke/tunnel/util/BackendException.kt b/tunnel/src/main/java/com/zaneschepke/tunnel/util/BackendException.kt index 8f244726..d24677b7 100644 --- a/tunnel/src/main/java/com/zaneschepke/tunnel/util/BackendException.kt +++ b/tunnel/src/main/java/com/zaneschepke/tunnel/util/BackendException.kt @@ -1,7 +1,6 @@ package com.zaneschepke.tunnel.util sealed class BackendException : Exception() { - class StateConflict(override val message: String) : BackendException() class InternalError(override val message: String) : BackendException()