feat: improved notifications with actions (#481)

This commit is contained in:
Zane Schepke 2024-12-09 22:38:55 -05:00 committed by GitHub
parent 670d9d680c
commit c5b42b55c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 312 additions and 233 deletions

View File

@ -204,5 +204,10 @@
<action android:name="com.wireguard.android.action.REFRESH_TUNNEL_STATES" />
</intent-filter>
</receiver>
<receiver
android:name=".receiver.NotificationActionReceiver"
android:exported="false"
android:permission="${applicationId}.permission.CONTROL_TUNNELS">
</receiver>
</application>
</manifest>

View File

@ -2,6 +2,8 @@ package com.zaneschepke.wireguardautotunnel.module
import android.content.Context
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.notification.WireGuardNotification
import com.zaneschepke.logcatter.LogcatReader
import dagger.Module
import dagger.Provides
@ -27,4 +29,10 @@ class AppModule {
fun provideLogCollect(@ApplicationContext context: Context): LogReader {
return LogcatReader.init(storageDir = context.filesDir.absolutePath)
}
@Singleton
@Provides
fun provideNotificationService(@ApplicationContext context: Context): NotificationService {
return WireGuardNotification(context)
}
}

View File

@ -4,8 +4,6 @@ import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.notification.WireGuardNotification
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@ -15,10 +13,6 @@ import dagger.hilt.android.scopes.ServiceScoped
@Module
@InstallIn(ServiceComponent::class)
abstract class ServiceModule {
@Binds
@ServiceScoped
abstract fun provideNotificationService(wireGuardNotification: WireGuardNotification): NotificationService
@Binds
@ServiceScoped
abstract fun provideWifiService(wifiService: WifiService): NetworkService<WifiService>

View File

@ -10,6 +10,7 @@ import com.wireguard.android.util.ToolsInstaller
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel
import dagger.Module
@ -76,6 +77,7 @@ class TunnelModule {
@ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
serviceManager: ServiceManager,
notificationService: NotificationService,
): TunnelService {
return WireGuardTunnel(
amneziaBackend,
@ -85,12 +87,17 @@ class TunnelModule {
applicationScope,
ioDispatcher,
serviceManager,
notificationService,
)
}
@Singleton
@Provides
fun provideServiceManager(@ApplicationContext context: Context): ServiceManager {
return ServiceManager.getInstance(context)
fun provideServiceManager(
@ApplicationContext context: Context,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
appDataRepository: AppDataRepository,
): ServiceManager {
return ServiceManager(context, ioDispatcher, appDataRepository)
}
}

View File

@ -5,8 +5,8 @@ import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@ -26,6 +26,9 @@ class KernelReceiver : BroadcastReceiver() {
@Inject
lateinit var tunnelConfigRepository: TunnelConfigRepository
@Inject
lateinit var serviceManager: ServiceManager
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action ?: return
applicationScope.launch {
@ -37,7 +40,7 @@ class KernelReceiver : BroadcastReceiver() {
tunnelConfigRepository.save(it.copy(isActive = true))
}
}
context.requestTunnelTileServiceStateUpdate()
serviceManager.updateTunnelTile()
}
}
}

View File

@ -0,0 +1,36 @@
package com.zaneschepke.wireguardautotunnel.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationAction
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
class NotificationActionReceiver : BroadcastReceiver() {
@Inject
lateinit var serviceManager: ServiceManager
@Inject
lateinit var tunnelService: TunnelService
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
override fun onReceive(context: Context, intent: Intent) {
applicationScope.launch {
when (intent.action) {
NotificationAction.AUTO_TUNNEL_OFF.name -> serviceManager.stopAutoTunnel()
NotificationAction.TUNNEL_OFF.name -> tunnelService.stopTunnel()
}
}
}
}

View File

@ -3,20 +3,25 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.Service
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.util.SingletonHolder
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import jakarta.inject.Inject
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext
import timber.log.Timber
@OptIn(ExperimentalCoroutinesApi::class)
class ServiceManager
@Inject constructor(private val context: Context) {
@Inject constructor(private val context: Context, private val ioDispatcher: CoroutineDispatcher, private val appDataRepository: AppDataRepository) {
private val _autoTunnelActive = MutableStateFlow(false)
@ -24,8 +29,8 @@ class ServiceManager
var autoTunnelService = CompletableDeferred<AutoTunnelService>()
var backgroundService = CompletableDeferred<TunnelBackgroundService>()
companion object : SingletonHolder<ServiceManager, Context>(::ServiceManager)
var autoTunnelTile = CompletableDeferred<AutoTunnelControlTile>()
var tunnelControlTile = CompletableDeferred<TunnelControlTile>()
private fun <T : Service> startService(cls: Class<T>, background: Boolean) {
runCatching {
@ -39,12 +44,15 @@ class ServiceManager
}
suspend fun startAutoTunnel(background: Boolean) {
val settings = appDataRepository.settings.getSettings()
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = true))
if (autoTunnelService.isCompleted) return _autoTunnelActive.update { true }
kotlin.runCatching {
startService(AutoTunnelService::class.java, background)
autoTunnelService.await()
autoTunnelService.getCompleted().start()
_autoTunnelActive.update { true }
updateAutoTunnelTile()
}.onFailure {
Timber.e(it)
}
@ -70,17 +78,41 @@ class ServiceManager
}
}
fun stopAutoTunnel() {
if (!autoTunnelService.isCompleted) return
runCatching {
autoTunnelService.getCompleted().stop()
_autoTunnelActive.update { false }
}.onFailure {
Timber.e(it)
suspend fun toggleAutoTunnel(background: Boolean) {
withContext(ioDispatcher) {
if (_autoTunnelActive.value) return@withContext stopAutoTunnel()
startAutoTunnel(background)
}
}
fun requestTunnelTileUpdate() {
context.requestTunnelTileServiceStateUpdate()
fun updateAutoTunnelTile() {
if (autoTunnelTile.isCompleted) {
autoTunnelTile.getCompleted().updateTileState()
} else {
context.requestAutoTunnelTileServiceUpdate()
}
}
fun updateTunnelTile() {
if (tunnelControlTile.isCompleted) {
tunnelControlTile.getCompleted().updateTileState()
} else {
context.requestTunnelTileServiceStateUpdate()
}
}
suspend fun stopAutoTunnel() {
withContext(ioDispatcher) {
val settings = appDataRepository.settings.getSettings()
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = false))
if (!autoTunnelService.isCompleted) return@withContext
runCatching {
autoTunnelService.getCompleted().stop()
_autoTunnelActive.update { false }
updateAutoTunnelTile()
}.onFailure {
Timber.e(it)
}
}
}
}

View File

@ -7,6 +7,7 @@ import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.util.Constants
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred
@ -21,8 +22,6 @@ class TunnelBackgroundService : LifecycleService() {
@Inject
lateinit var serviceManager: ServiceManager
private val foregroundId = 123
override fun onCreate() {
super.onCreate()
start()
@ -42,7 +41,7 @@ class TunnelBackgroundService : LifecycleService() {
fun start() {
ServiceCompat.startForeground(
this,
foregroundId,
NotificationService.KERNEL_SERVICE_NOTIFICATION_ID,
createNotification(),
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
)
@ -60,8 +59,7 @@ class TunnelBackgroundService : LifecycleService() {
private fun createNotification(): Notification {
return notificationService.createNotification(
getString(R.string.vpn_channel_id),
getString(R.string.vpn_channel_name),
WireGuardNotification.NotificationChannels.VPN,
getString(R.string.tunnel_running),
description = "",
)

View File

@ -24,7 +24,9 @@ import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationAction
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
@ -53,7 +55,6 @@ import javax.inject.Provider
@AndroidEntryPoint
class AutoTunnelService : LifecycleService() {
private val foregroundId = 122
@Inject
@AppShell
@ -147,14 +148,16 @@ class AutoTunnelService : LifecycleService() {
private fun launchWatcherNotification(description: String = getString(R.string.monitoring_state_changes)) {
val notification =
notificationService.createNotification(
channelId = getString(R.string.watcher_channel_id),
channelName = getString(R.string.watcher_channel_name),
WireGuardNotification.NotificationChannels.AUTO_TUNNEL,
title = getString(R.string.auto_tunnel_title),
description = description,
actions = listOf(
notificationService.createNotificationAction(NotificationAction.AUTO_TUNNEL_OFF),
),
)
ServiceCompat.startForeground(
this,
foregroundId,
NotificationService.AUTO_TUNNEL_NOTIFICATION_ID,
notification,
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
)

View File

@ -0,0 +1,17 @@
package com.zaneschepke.wireguardautotunnel.service.notification
import android.content.Context
import com.zaneschepke.wireguardautotunnel.R
enum class NotificationAction {
TUNNEL_OFF,
AUTO_TUNNEL_OFF,
;
fun title(context: Context): String {
return when (this) {
TUNNEL_OFF -> context.getString(R.string.stop)
AUTO_TUNNEL_OFF -> context.getString(R.string.stop)
}
}
}

View File

@ -2,21 +2,32 @@ package com.zaneschepke.wireguardautotunnel.service.notification
import android.app.Notification
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import androidx.core.app.NotificationCompat
import com.zaneschepke.wireguardautotunnel.service.notification.WireGuardNotification.NotificationChannels
interface NotificationService {
val context: Context
fun createNotification(
channelId: String,
channelName: String,
channel: NotificationChannels,
title: String = "",
action: PendingIntent? = null,
actionText: String? = null,
description: String,
actions: Collection<NotificationCompat.Action> = emptyList(),
description: String = "",
showTimestamp: Boolean = false,
importance: Int = NotificationManager.IMPORTANCE_HIGH,
vibration: Boolean = false,
onGoing: Boolean = true,
lights: Boolean = true,
onlyAlertOnce: Boolean = true,
): Notification
fun createNotificationAction(action: NotificationAction): NotificationCompat.Action
fun remove(notificationId: Int)
fun show(notificationId: Int, notification: Notification)
companion object {
const val KERNEL_SERVICE_NOTIFICATION_ID = 123
const val AUTO_TUNNEL_NOTIFICATION_ID = 122
const val VPN_NOTIFICATION_ID = 100
}
}

View File

@ -1,105 +1,134 @@
package com.zaneschepke.wireguardautotunnel.service.notification
import android.Manifest
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.MainActivity
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
class WireGuardNotification
@Inject
constructor(
@ApplicationContext private val context: Context,
) :
NotificationService {
private val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@ApplicationContext override val context: Context,
) : NotificationService {
private val watcherBuilder: NotificationCompat.Builder =
NotificationCompat.Builder(
context,
context.getString(R.string.watcher_channel_id),
)
private val tunnelBuilder: NotificationCompat.Builder =
NotificationCompat.Builder(
context,
context.getString(R.string.vpn_channel_id),
)
enum class NotificationChannels {
VPN,
AUTO_TUNNEL,
}
private val notificationManager = NotificationManagerCompat.from(context)
override fun createNotification(
channelId: String,
channelName: String,
channel: NotificationChannels,
title: String,
action: PendingIntent?,
actionText: String?,
actions: Collection<NotificationCompat.Action>,
description: String,
showTimestamp: Boolean,
importance: Int,
vibration: Boolean,
onGoing: Boolean,
lights: Boolean,
onlyAlertOnce: Boolean,
): Notification {
val channel =
NotificationChannel(
channelId,
channelName,
importance,
)
.let {
it.description = title
it.enableLights(lights)
it.lightColor = Color.RED
it.enableVibration(vibration)
it.vibrationPattern = longArrayOf(100, 200, 300)
it
}
notificationManager.createNotificationChannel(channel)
val pendingIntent: PendingIntent =
Intent(context, MainActivity::class.java).let { notificationIntent ->
PendingIntent.getActivity(
notificationManager.createNotificationChannel(channel.asChannel())
return channel.asBuilder().apply {
actions.forEach {
addAction(it)
}
setContentTitle(title)
setContentText(description)
setOnlyAlertOnce(onlyAlertOnce)
setOngoing(onGoing)
setPriority(NotificationCompat.PRIORITY_HIGH)
setShowWhen(showTimestamp)
setSmallIcon(R.drawable.ic_launcher)
}.build()
}
override fun createNotificationAction(notificationAction: NotificationAction): NotificationCompat.Action {
val pendingIntent = PendingIntent.getBroadcast(
context,
0,
Intent(context, NotificationActionReceiver::class.java).apply {
action = notificationAction.name
},
PendingIntent.FLAG_IMMUTABLE,
)
return NotificationCompat.Action.Builder(
R.drawable.ic_launcher,
notificationAction.title(context).uppercase(),
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)
}
}
fun NotificationChannels.asBuilder(): NotificationCompat.Builder {
return when (this) {
NotificationChannels.VPN -> {
NotificationCompat.Builder(
context,
0,
notificationIntent,
PendingIntent.FLAG_IMMUTABLE,
context.getString(R.string.auto_tunnel_channel_id),
)
}
NotificationChannels.AUTO_TUNNEL -> {
NotificationCompat.Builder(
context,
context.getString(R.string.vpn_channel_id),
)
}
}
}
val builder =
when (channelId) {
context.getString(R.string.watcher_channel_id) -> watcherBuilder
context.getString(R.string.vpn_channel_id) -> tunnelBuilder
else -> {
NotificationCompat.Builder(
context,
channelId,
)
fun NotificationChannels.asChannel(): NotificationChannel {
return when (this) {
NotificationChannels.VPN -> {
NotificationChannel(
context.getString(R.string.vpn_channel_id),
context.getString(R.string.vpn_channel_name),
NotificationManager.IMPORTANCE_HIGH,
).apply {
description = context.getString(R.string.vpn_channel_description)
enableLights(true)
lightColor = Color.WHITE
enableVibration(false)
vibrationPattern = longArrayOf(100, 200, 300)
}
}
return builder.let {
if (action != null && actionText != null) {
it.addAction(
NotificationCompat.Action.Builder(0, actionText, action).build(),
)
it.setAutoCancel(true)
NotificationChannels.AUTO_TUNNEL -> {
NotificationChannel(
context.getString(R.string.auto_tunnel_channel_id),
context.getString(R.string.auto_tunnel_channel_name),
NotificationManager.IMPORTANCE_HIGH,
).apply {
description = context.getString(R.string.auto_tunnel_channel_description)
enableLights(true)
lightColor = Color.WHITE
enableVibration(false)
vibrationPattern = longArrayOf(100, 200, 300)
}
}
it.setContentTitle(title)
.setContentText(description)
.setOnlyAlertOnce(onlyAlertOnce)
.setContentIntent(pendingIntent)
.setOngoing(onGoing)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setShowWhen(showTimestamp)
.setSmallIcon(R.drawable.ic_launcher)
.build()
}
}
}

View File

@ -1,24 +1,18 @@
package com.zaneschepke.wireguardautotunnel.service.tile
import android.content.Intent
import android.os.IBinder
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class AutoTunnelControlTile : TileService(), LifecycleOwner {
class AutoTunnelControlTile : TileService() {
@Inject
lateinit var appDataRepository: AppDataRepository
@ -29,32 +23,26 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
@ApplicationScope
lateinit var applicationScope: CoroutineScope
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
override fun onCreate() {
super.onCreate()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
}
override fun onStopListening() {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
serviceManager.autoTunnelTile.complete(this)
}
override fun onDestroy() {
super.onDestroy()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
serviceManager.autoTunnelTile = CompletableDeferred()
}
override fun onStartListening() {
super.onStartListening()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
lifecycleScope.launch {
serviceManager.autoTunnelTile.complete(this)
applicationScope.launch {
if (appDataRepository.tunnels.getAll().isEmpty()) return@launch setUnavailable()
updateTileState()
}
}
private fun updateTileState() {
fun updateTileState() {
serviceManager.autoTunnelActive.value.let {
if (it) setActive() else setInactive()
}
@ -63,7 +51,7 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
override fun onClick() {
super.onClick()
unlockAndRun {
lifecycleScope.launch {
applicationScope.launch {
if (serviceManager.autoTunnelActive.value) {
serviceManager.stopAutoTunnel()
setInactive()
@ -95,18 +83,4 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
qsTile.updateTile()
}
}
/* This works around an annoying unsolved frameworks bug some people are hitting. */
override fun onBind(intent: Intent): IBinder? {
var ret: IBinder? = null
try {
ret = super.onBind(intent)
} catch (_: Throwable) {
Timber.e("Failed to bind to TunnelControlTile")
}
return ret
}
override val lifecycle: Lifecycle
get() = lifecycleRegistry
}

View File

@ -1,27 +1,22 @@
package com.zaneschepke.wireguardautotunnel.service.tile
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Provider
@AndroidEntryPoint
class TunnelControlTile : TileService(), LifecycleOwner {
class TunnelControlTile : TileService() {
@Inject
lateinit var appDataRepository: AppDataRepository
@ -32,33 +27,29 @@ class TunnelControlTile : TileService(), LifecycleOwner {
@ApplicationScope
lateinit var applicationScope: CoroutineScope
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
@Inject
lateinit var serviceManager: ServiceManager
override fun onCreate() {
super.onCreate()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
}
override fun onStopListening() {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
serviceManager.tunnelControlTile.complete(this)
}
override fun onDestroy() {
super.onDestroy()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
serviceManager.tunnelControlTile = CompletableDeferred()
}
override fun onStartListening() {
super.onStartListening()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
Timber.d("Updating tile!")
lifecycleScope.launch {
serviceManager.tunnelControlTile.complete(this)
applicationScope.launch {
if (appDataRepository.tunnels.getAll().isEmpty()) return@launch setUnavailable()
updateTileState()
}
}
private suspend fun updateTileState() {
fun updateTileState() = applicationScope.launch {
val lastActive = appDataRepository.getStartTunnelConfig()
lastActive?.let {
updateTile(it)
@ -68,7 +59,7 @@ class TunnelControlTile : TileService(), LifecycleOwner {
override fun onClick() {
super.onClick()
unlockAndRun {
lifecycleScope.launch {
applicationScope.launch {
val lastActive = appDataRepository.getStartTunnelConfig()
lastActive?.let { tunnel ->
if (tunnel.isActive) {
@ -125,18 +116,4 @@ class TunnelControlTile : TileService(), LifecycleOwner {
}
}
}
/* This works around an annoying unsolved frameworks bug some people are hitting. */
override fun onBind(intent: Intent): IBinder? {
var ret: IBinder? = null
try {
ret = super.onBind(intent)
} catch (_: Throwable) {
Timber.e("Failed to bind to TunnelControlTile")
}
return ret
}
override val lifecycle: Lifecycle
get() = lifecycleRegistry
}

View File

@ -9,6 +9,8 @@ import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.module.Kernel
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.AmneziaStatistics
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.WireGuardStatistics
@ -27,6 +29,9 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.amnezia.awg.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationAction
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService.Companion.VPN_NOTIFICATION_ID
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Provider
@ -41,6 +46,7 @@ constructor(
@ApplicationScope private val applicationScope: CoroutineScope,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val serviceManager: ServiceManager,
private val notificationService: NotificationService,
) : TunnelService {
private val _vpnState = MutableStateFlow(VpnState())
@ -116,6 +122,16 @@ constructor(
setState(tunnelConfig, TunnelState.UP).onSuccess {
startActiveTunnelJobs()
if (it.isUp()) appDataRepository.tunnels.save(tunnelConfig.copy(isActive = true))
with(notificationService) {
val notification = createNotification(
WireGuardNotification.NotificationChannels.VPN,
title = "${context.getString(R.string.tunnel_running)} - ${tunnelConfig.name}",
actions = listOf(
notificationService.createNotificationAction(NotificationAction.TUNNEL_OFF),
),
)
show(VPN_NOTIFICATION_ID, notification)
}
updateTunnelState(it, tunnelConfig)
}
}.onFailure {
@ -134,6 +150,7 @@ constructor(
setState(tunnelConfig, TunnelState.DOWN).onSuccess {
updateTunnelState(it, null)
onStop(tunnelConfig)
notificationService.remove(VPN_NOTIFICATION_ID)
stopBackgroundService()
}.onFailure {
Timber.e(it)
@ -161,9 +178,7 @@ constructor(
}
callback()
}
is Backend -> {
callback()
}
is Backend -> callback()
}
}
@ -181,9 +196,7 @@ constructor(
is org.amnezia.awg.backend.Backend -> {
backend.backendState.asBackendState()
}
is Backend -> {
BackendState.SERVICE_ACTIVE
}
is Backend -> BackendState.SERVICE_ACTIVE
else -> BackendState.INACTIVE
}
}
@ -213,12 +226,12 @@ constructor(
private suspend fun startBackgroundService() {
serviceManager.startBackgroundService()
serviceManager.requestTunnelTileUpdate()
serviceManager.updateTunnelTile()
}
private fun stopBackgroundService() {
serviceManager.stopBackgroundService()
serviceManager.requestTunnelTileUpdate()
serviceManager.updateTunnelTile()
}
private suspend fun onBeforeStart(background: Boolean) {
@ -320,14 +333,14 @@ constructor(
_vpnState.update {
it.copy(status = TunnelState.from(newState))
}
serviceManager.requestTunnelTileUpdate()
serviceManager.updateTunnelTile()
}
override fun onStateChange(state: State) {
_vpnState.update {
it.copy(status = TunnelState.from(state))
}
serviceManager.requestTunnelTileUpdate()
serviceManager.updateTunnelTile()
}
companion object {

View File

@ -159,18 +159,7 @@ constructor(
}
fun onToggleAutoTunnel() = viewModelScope.launch {
val settings = appDataRepository.settings.getSettings()
val toggled = !settings.isAutoTunnelEnabled
if (toggled) {
serviceManager.startAutoTunnel(false)
} else {
serviceManager.stopAutoTunnel()
}
appDataRepository.settings.save(
settings.copy(
isAutoTunnelEnabled = toggled,
),
)
serviceManager.toggleAutoTunnel(false)
}
private suspend fun saveTunnelsFromZipUri(uri: Uri, context: Context) {

View File

@ -81,7 +81,6 @@
<string name="version">Version</string>
<string name="settings">Einstellungen</string>
<string name="support">Unterstützung</string>
<string name="watcher_channel_id">Wächterkanal</string>
<string name="error_authentication_failed">Authentifizierung fehlgeschlagen</string>
<string name="export_configs">Konfigurationen exportieren</string>
<string name="unknown_error">Unbekannter Fehler aufgetreten</string>
@ -99,7 +98,6 @@
<string name="set_primary_tunnel">Als Primären Tunnel setzen</string>
<string name="vpn_channel_id">VPN Kanal</string>
<string name="vpn_channel_name">VPN Benachrichtigungskanal</string>
<string name="watcher_channel_name">Wächterbenachrichtigungskanal</string>
<string name="turn_off_tunnel">Aktion erfordert deaktivierten Tunnel</string>
<string name="kernel">Kernel</string>
<string name="use_kernel">Kernelmodul verwenden</string>
@ -171,4 +169,4 @@
<string name="use_wildcards">Wildcards für Namen verwenden</string>
<string name="stop_auto">Auto-Tunnel stoppen</string>
<string name="monitoring_state_changes">Überwache Statusänderungen</string>
</resources>
</resources>

View File

@ -101,10 +101,8 @@
<string name="app_name">WG Tunnel</string>
<string name="vpn_channel_id">Canal VPN</string>
<string name="vpn_channel_name">Canal de notificación VPN</string>
<string name="watcher_channel_id">Canal del obvervador</string>
<string name="watcher_channel_name">Canal de notificación del obvervador</string>
<string name="prominent_background_location_message">La monitorización SSID Wi-Fi necesita de permiso de ubicación en segundo plano incluso si la app está cerrada. Mira el enlace a la Política de Privacidad en la pantalla de ayuda para más detalles.</string>
<string name="prominent_background_location_message">La monitorización SSID Wi-Fi necesita de permiso de ubicación en segundo plano incluso si la app está cerrada. Mira el enlace a la Política de Privacidad en la pantalla de ayuda para más detalles.</string>
<string name="junk_packet_count">Recuento de paquetes basura</string>
<string name="junk_packet_minimum_size">Tamaño mínimo del paquete basura</string>
<string name="add_from_clipboard">Agregar desde el portapapeles</string>
</resources>
</resources>

View File

@ -1,8 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="vpn_channel_id">Canal VPN</string>
<string name="watcher_channel_id">Canal de surveillance</string>
<string name="watcher_channel_name">Canal de notification de surveillance</string>
<string name="turn_off_tunnel">Cette action nécessite la désactivation du tunnel</string>
<string name="no_tunnels">Aucun tunnel n\'a été ajouté pour le moment!</string>
<string name="tunnels">Tunnels</string>
@ -171,4 +169,4 @@
<string name="kernel_not_supported">Noyau non supporté</string>
<string name="start_auto">Démarrer l\'auto-tunnel</string>
<string name="requires_app_relaunch">Cette modification nécessite un redémarrage de l\'application. Voulez-vous continuer?</string>
</resources>
</resources>

View File

@ -6,7 +6,6 @@
<string name="kernel">Kernel</string>
<string name="init_packet_junk_size">Ukuran sampah paket init</string>
<string name="app_name">WG Tunnel</string>
<string name="watcher_channel_name">Notifikasi Saluran Pengamat</string>
<string name="error_file_extension">File bukan .conf atau .zip</string>
<string name="turn_off_tunnel">Aksi memerlukan tunnel mati</string>
<string name="no_tunnels">Belum ada tunnel yang ditambahkan!</string>
@ -99,7 +98,6 @@
<string name="handshake">handshake</string>
<string name="vpn_channel_id">Saluran VPN</string>
<string name="vpn_channel_name">Notifikasi Saluran VPN</string>
<string name="watcher_channel_id">Saluran Pengamat</string>
<string name="prominent_background_location_message">Fitur ini memerlukan izin lokasi latar belakang untuk mengaktifkan pemantauan SSID Wi-Fi bahkan saat aplikasi ditutup. Untuk detail lebih lanjut, silakan lihat Kebijakan Privasi yang ditautkan di layar Dukungan.</string>
<string name="copy_public_key">Salin kunci publik</string>
<string name="base64_key">kunci base64</string>
@ -117,4 +115,4 @@
<string name="create_pin">Buat pin</string>
<string name="mobile_data_tunnel">Ditetapkan sebagai tunnel data seluler</string>
<string name="set_primary_tunnel">Ditetapkan sebagai tunnel utama</string>
</resources>
</resources>

View File

@ -15,8 +15,6 @@
<string name="icon">Icoon</string>
<string name="include">Meenemen</string>
<string name="addresses">Adres</string>
<string name="watcher_channel_id">Watcher Kanaal</string>
<string name="watcher_channel_name">Watcher Notificatiekanaal</string>
<string name="peer">Peer (extern systeem)</string>
<string name="allowed_ips">Allowed IPs</string>
<string name="always_on_vpn_support">Altijd-aan VPN toestaan</string>
@ -138,4 +136,4 @@
<string name="location_services_missing_message">De app kan geen ingeschakelde locatieservices op je apparaat vinden. Afhankelijk van het type apparaat kan dit leiden tot een niet functionerende herkenning van het verbonden WiFi netwerk. De niet-vertrouwde WiFi functionaliteit werkt daardoor mogelijk niet. Toch doorgaan?</string>
<string name="background_location_message">Permanente achtergrondtoegang tot exacte locatie is vereist voor deze functie. Bekijk aub de</string>
<string name="transport_packet_magic_header">Transport packet magic header</string>
</resources>
</resources>

View File

@ -99,8 +99,6 @@
<string name="app_name">WG Tunnel</string>
<string name="vpn_channel_id">Canal de VPN</string>
<string name="vpn_channel_name">Canal de notificações VPN</string>
<string name="watcher_channel_id">Canal de vigia</string>
<string name="watcher_channel_name">Canal de notificações de vigia</string>
<string name="prominent_background_location_message">Este recurso precisa de permissões de localização em segundo plano para ativar o monitoramento do SSID da rede Wi-Fi mesmo quando a aplicação está fechado. Para mais pormenores, por favor veja a Política de Privacidade no ecrã de Suporte.</string>
<string name="trusted_ssid_value_description">Envie o SSID</string>
<string name="add_tunnels_text">Adicionar a partir de ficheiro ou zip</string>
@ -132,4 +130,4 @@
<string name="getting_started_guide">guia de início rápido</string>
<string name="always_on_message2">para ter certeza que VPN Sempre-ligada é desligada para todas as outras aplicações e tente novamente</string>
<string name="restart_at_boot">Reiniciar no arranque</string>
</resources>
</resources>

View File

@ -110,8 +110,6 @@
<string name="error_file_format">Formato de configuração inválido</string>
<string name="vpn_channel_id">Canal de VPN</string>
<string name="vpn_channel_name">Canal de notificações VPN</string>
<string name="watcher_channel_id">Canal de vigia</string>
<string name="watcher_channel_name">Canal de notificações de vigia</string>
<string name="db_name">wg-tunnel-db</string>
<string name="set_custom_ping_ip">Definir ip ping personalizado</string>
<string name="vpn_denied_dialog_title">Permissão negada</string>
@ -130,4 +128,4 @@
<string name="handshake">handshake</string>
<string name="background_location_message">Permitir que toda a permissão de localização do tempo e/ou localização precisa é necessária para este recurso. Por favor, veja</string>
<string name="sec">seg</string>
</resources>
</resources>

View File

@ -63,8 +63,6 @@
<string name="app_name">WG Tunnel</string>
<string name="vpn_channel_id">Канал VPN</string>
<string name="vpn_channel_name">Канал уведомлений VPN</string>
<string name="watcher_channel_id">Канал наблюдателя</string>
<string name="watcher_channel_name">Канал уведомлений наблюдателя</string>
<string name="tunnels">Туннели</string>
<string name="okay">Хорошо</string>
<string name="prominent_background_location_title">Фоновая передача местоположения</string>
@ -171,4 +169,4 @@
<string name="monitoring_state_changes">Отслеживание изменений состояния</string>
<string name="enable_local_logging">Включить ведение журнала</string>
<string name="add_from_clipboard">Добавить из буфера обмена</string>
</resources>
</resources>

View File

@ -3,8 +3,6 @@
<string name="app_name">WG Tunnel</string>
<string name="vpn_channel_id">VPN Kanalı</string>
<string name="vpn_channel_name">VPN Bildirim Kanalı</string>
<string name="watcher_channel_id">İzleyici Kanalı</string>
<string name="watcher_channel_name">İzleyici Bildirim Kanalı</string>
<string name="github_url" translatable="false">https://github.com/zaneschepke/wgtunnel/issues</string>
<string name="docs_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/overview.html</string>
<string name="privacy_policy_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/privacypolicy.html</string>
@ -124,4 +122,4 @@
<string name="getting_started_guide">başlangıç kılavuzu</string>
<string name="error_file_format">Geçersiz tünel yapılandırma formatı</string>
<string name="restart_at_boot">Önyüklemede yeniden başlat</string>
</resources>
</resources>

View File

@ -97,12 +97,10 @@
<string name="no_browser_detected">没有安装浏览器</string>
<string name="incorrect_pin">密码不正确</string>
<string name="set_custom_ping_ip">自定义 Ping 的目标 ip</string>
<string name="watcher_channel_name">守护者通知频道</string>
<string name="vpn_channel_id">VPN 频道</string>
<string name="junk_packet_count">无效包计数</string>
<string name="app_name">WG Tunnel</string>
<string name="vpn_channel_name">VPN 通知频道</string>
<string name="watcher_channel_id">守护者频道</string>
<string name="open_issue">查看问题</string>
<string name="read_logs">查看日志</string>
<string name="auto">(自动)</string>
@ -171,4 +169,4 @@
<string name="enable_local_logging">开启本地日志</string>
<string name="configuration_change">配置更改</string>
<string name="requires_app_relaunch">此更改需要重新启动应用程序。您是否要继续?</string>
</resources>
</resources>

View File

@ -2,9 +2,7 @@
<string name="app_name">WG Tunnel</string>
<string name="vpn_channel_id">VPN Channel</string>
<string name="vpn_channel_name">VPN Notification Channel</string>
<string name="watcher_channel_id">Watcher Channel</string>
<string name="watcher_channel_name">Watcher Notification Channel</string>
<string name="github_url" translatable="false">https://github.com/zaneschepke/wgtunnel/issues</string>
<string name="github_url" translatable="false">https://github.com/zaneschepke/wgtunnel/issues</string>
<string name="docs_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/overview.html</string>
<string name="privacy_policy_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/privacypolicy.html</string>
<string name="docs_wildcards" translatable="false" >https://zaneschepke.com/wgtunnel-docs/features.html#wildcard-wi-fi-name-support</string>
@ -82,7 +80,7 @@
<string name="error_no_file_explorer">No file explorer installed</string>
<string name="error_invalid_code">Invalid QR code</string>
<string name="location_services_missing_message">The app is not detecting any location services enabled on your device. Depending on the device, this could cause the untrusted wifi feature to fail to read the wifi name. Would you like to continue anyways?</string>
<string name="auto_tunnel_title">Auto-tunnel Service</string>
<string name="auto_tunnel_title">Auto-tunnel service</string>
<string name="delete_tunnel">Delete tunnel</string>
<string name="delete_tunnel_message">Are you sure you would like to delete this tunnel?</string>
<string name="yes">Yes</string>
@ -187,4 +185,9 @@
<string name="kill_switch_options">Kill switch options</string>
<string name="allow_lan_traffic">Allow LAN traffic</string>
<string name="bypass_lan_for_kill_switch">Bypass LAN for kill switch</string>
<string name="vpn_channel_description">A channel for VPN state notifications</string>
<string name="auto_tunnel_channel_id">Auto-tunnel Channel</string>
<string name="auto_tunnel_channel_name">Auto-tunnel Notification Channel</string>
<string name="auto_tunnel_channel_description">A channel for auto-tunnel state notifications</string>
<string name="stop">stop</string>
</resources>