Compare commits

..

11 Commits

Author SHA1 Message Date
Zane Schepke 69b07eec6f feat: support Android 8
Add support for Android 8.

Fix shortcuts bug where it was toggling auto-tunneling without setting enabled.

Fix shortcuts name not updating with config edits.

Bump versions.

Closes #25
2023-09-12 12:48:54 -04:00
Zane Schepke e81066f508 fix: FireTV file selection bug
Fixes bug where FireTV devices were unable to launch a proper file browser to select tunnel configs.
2023-09-11 11:15:44 -04:00
Zane Schepke 64bb9f3b82 fix: foreground service start crashes
Attempt to fix startForegrounService causing crashes on some devices by not meeting the 5 second notification rule. Add notification to onCreate of services.

Limit startForeground to only be called where it is truly necessary in the TileService to allow starting the VPN while app is not running.

Attempt to manually initialize mlkit for QR code scanning to remediate some crashes caused by config scanning.
2023-09-10 00:23:23 -04:00
Zane Schepke c1b560e822 chore: update README.md 2023-09-08 12:43:41 -04:00
Zane Schepke 14fe5821cc Merge branch 'main' of github.com:zaneschepke/wgtunnel 2023-09-04 05:52:27 -04:00
Zane Schepke 9d9b7bebca fix: foreground service crash on older devices
Fixes a bug where older device take a longer time to launch the foreground service and connect to the VPN. Combined with a delayed launch of foreground notification until VPN connection is confirmed, this would break foreground service,s 5 second notification rule.

Fixed by adding a new attempting connection notification to launch on vpn initial connection attempt.
2023-09-04 05:52:15 -04:00
Zane Schepke 12d1ccc084 chore: update README.md 2023-09-04 02:51:05 -04:00
Zane Schepke 20cc2c09b0 feat: dynamic shortcuts and package search
Add dynamic on and off shortcuts for each new tunnel added. Shortcuts will toggle auto tunneling to allow manual override.

Add package search for split tunneling.

Add tunnel toggling from tile even while app is closed and auto tunneling is disabled.

Refactor and code cleanup.

Closes #23 Closes #21
2023-09-04 02:42:49 -04:00
Zane Schepke eeccc71469 fix: tile auto tunnel toggle and wifi internet connection check
Change tile toggle behavior to also toggle auto tunnel service to allow user temporary override and re-enablement of auto tunnel from the tile.

Add tunnel name to tile.

Fix bug where wifi networks without internet access were impacting auto tunneling determinations.

Closes #22
2023-09-01 13:05:17 -04:00
Zane Schepke 0e64bbb4e1 feat: add quick setting tile
Add quick settings tile for easy tunnel toggling and auto-tunnel override.

Fix bug on AndroidTV D-pad tunnel control for multiple tunnels.

Closes #18 , Closes #20
2023-08-30 23:58:03 -04:00
Zane Schepke f513297ba0 fix: file selection on older devices
Fixes bug where file selection was causing app to crash on older devices.
2023-08-11 21:13:54 -04:00
31 changed files with 709 additions and 243 deletions
+4 -1
View File
@@ -13,6 +13,7 @@ WG Tunnel
[![Google Play](https://img.shields.io/badge/Google_Play-414141?style=for-the-badge&logo=google-play&logoColor=white)](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel) [![Google Play](https://img.shields.io/badge/Google_Play-414141?style=for-the-badge&logo=google-play&logoColor=white)](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel)
[![Fire TV](https://img.shields.io/badge/fire%20tv-fc3b2d?style=for-the-badge&logo=amazon%20fire%20tv&logoColor=white)](https://www.amazon.com/gp/product/B0CFGGL7WK)
</span> </span>
@@ -50,8 +51,10 @@ The inspiration for this app came from the inconvenience of constantly having to
* Add tunnels via .conf file * Add tunnels via .conf file
* Auto connect to VPN based on Wi-Fi SSID * Auto connect to VPN based on Wi-Fi SSID
* Split tunneling by application * Split tunneling by application with search
* Always-on VPN for Android support * Always-on VPN for Android support
* Quick tile support for vpn toggling
* Dynamic shortcuts support for automation integration
* Configurable Trusted Network list * Configurable Trusted Network list
* Optional auto connect on mobile data * Optional auto connect on mobile data
* Automatic service restart after reboot * Automatic service restart after reboot
+8 -8
View File
@@ -16,13 +16,13 @@ android {
compileSdk = 34 compileSdk = 34
val versionMajor = 2 val versionMajor = 2
val versionMinor = 3 val versionMinor = 4
val versionPatch = 4 val versionPatch = 5
val versionBuild = 0 val versionBuild = 0
defaultConfig { defaultConfig {
applicationId = "com.zaneschepke.wireguardautotunnel" applicationId = "com.zaneschepke.wireguardautotunnel"
minSdk = 28 minSdk = 26
targetSdk = 34 targetSdk = 34
versionCode = versionMajor * 10000 + versionMinor * 1000 + versionPatch * 100 + versionBuild versionCode = versionMajor * 10000 + versionMinor * 1000 + versionPatch * 100 + versionBuild
versionName = "${versionMajor}.${versionMinor}.${versionPatch}" versionName = "${versionMajor}.${versionMinor}.${versionPatch}"
@@ -64,8 +64,8 @@ android {
} }
dependencies { dependencies {
implementation("androidx.core:core-ktx:1.10.1") implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
implementation("androidx.activity:activity-compose:1.7.2") implementation("androidx.activity:activity-compose:1.7.2")
implementation(platform("androidx.compose:compose-bom:2023.03.00")) implementation(platform("androidx.compose:compose-bom:2023.03.00"))
implementation("androidx.compose.ui:ui") implementation("androidx.compose.ui:ui")
@@ -89,7 +89,7 @@ dependencies {
implementation("com.jakewharton.timber:timber:5.0.1") implementation("com.jakewharton.timber:timber:5.0.1")
// compose navigation // compose navigation
implementation("androidx.navigation:navigation-compose:2.7.0") implementation("androidx.navigation:navigation-compose:2.7.2")
implementation("androidx.hilt:hilt-navigation-compose:1.0.0") implementation("androidx.hilt:hilt-navigation-compose:1.0.0")
// hilt // hilt
@@ -107,10 +107,10 @@ dependencies {
implementation("io.objectbox:objectbox-kotlin:${rExtra.get("objectBoxVersion")}") implementation("io.objectbox:objectbox-kotlin:${rExtra.get("objectBoxVersion")}")
//lifecycle //lifecycle
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.1") implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.2")
//icons //icons
implementation("androidx.compose.material:material-icons-extended:1.5.0") implementation("androidx.compose.material:material-icons-extended:1.5.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
+20
View File
@@ -58,16 +58,35 @@
<category android:name="android.intent.category.LEANBACK_LAUNCHER" /> <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:finishOnTaskLaunch="true"
android:theme="@android:style/Theme.NoDisplay"
android:name=".service.shortcut.ShortcutsActivity"/>
<service <service
android:name=".service.foreground.ForegroundService" android:name=".service.foreground.ForegroundService"
android:enabled="true" android:enabled="true"
android:foregroundServiceType="remoteMessaging" android:foregroundServiceType="remoteMessaging"
android:exported="false"> android:exported="false">
</service> </service>
<service
android:exported="true"
android:name=".service.tile.TunnelControlTile"
android:icon="@drawable/shield"
android:label="WG Tunnel"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<meta-data android:name="android.service.quicksettings.ACTIVE_TILE"
android:value="true" />
<meta-data android:name="android.service.quicksettings.TOGGLEABLE_TILE"
android:value="true" />
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<service <service
android:name=".service.foreground.WireGuardTunnelService" android:name=".service.foreground.WireGuardTunnelService"
android:permission="android.permission.BIND_VPN_SERVICE" android:permission="android.permission.BIND_VPN_SERVICE"
android:enabled="true" android:enabled="true"
android:persistent="true"
android:foregroundServiceType="remoteMessaging" android:foregroundServiceType="remoteMessaging"
android:exported="false"> android:exported="false">
<intent-filter> <intent-filter>
@@ -80,6 +99,7 @@
android:name=".service.foreground.WireGuardConnectivityWatcherService" android:name=".service.foreground.WireGuardConnectivityWatcherService"
android:enabled="true" android:enabled="true"
android:stopWithTask="false" android:stopWithTask="false"
android:persistent="true"
android:foregroundServiceType="location" android:foregroundServiceType="location"
android:permission="" android:permission=""
android:exported="false"> android:exported="false">
@@ -4,6 +4,7 @@ import android.app.Application
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import com.google.firebase.crashlytics.FirebaseCrashlytics import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.mlkit.common.MlKit
import com.zaneschepke.wireguardautotunnel.repository.Repository import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
@@ -22,6 +23,11 @@ class WireGuardAutoTunnel : Application() {
FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(false); FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(false);
Timber.plant(Timber.DebugTree()) Timber.plant(Timber.DebugTree())
} }
try {
MlKit.initialize(this)
} catch (e : Exception) {
Timber.e(e.message)
}
settingsRepo.init() settingsRepo.init()
} }
@@ -3,11 +3,8 @@ package com.zaneschepke.wireguardautotunnel.receiver
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.repository.Repository import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.foreground.Action import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceTracker
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -30,12 +27,7 @@ class BootReceiver : BroadcastReceiver() {
if (!settings.isNullOrEmpty()) { if (!settings.isNullOrEmpty()) {
val setting = settings.first() val setting = settings.first()
if (setting.isAutoTunnelEnabled && setting.defaultTunnel != null) { if (setting.isAutoTunnelEnabled && setting.defaultTunnel != null) {
ServiceTracker.actionOnService( ServiceManager.startWatcherService(context, setting.defaultTunnel!!)
Action.START, context,
WireGuardConnectivityWatcherService::class.java,
mapOf(context.resources.getString(R.string.tunnel_extras_key) to
setting.defaultTunnel!!)
)
} }
} }
} finally { } finally {
@@ -3,11 +3,8 @@ package com.zaneschepke.wireguardautotunnel.receiver
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.repository.Repository import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.foreground.Action import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceTracker
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -29,23 +26,9 @@ class NotificationActionReceiver : BroadcastReceiver() {
if (!settings.isNullOrEmpty()) { if (!settings.isNullOrEmpty()) {
val setting = settings.first() val setting = settings.first()
if (setting.defaultTunnel != null) { if (setting.defaultTunnel != null) {
ServiceTracker.actionOnService( ServiceManager.stopVpnService(context)
Action.STOP, context,
WireGuardTunnelService::class.java,
mapOf(
context.resources.getString(R.string.tunnel_extras_key) to
setting.defaultTunnel!!
)
)
delay(1000) delay(1000)
ServiceTracker.actionOnService( ServiceManager.startVpnService(context, setting.defaultTunnel.toString())
Action.START, context,
WireGuardTunnelService::class.java,
mapOf(
context.resources.getString(R.string.tunnel_extras_key) to
setting.defaultTunnel!!
)
)
} }
} }
} finally { } finally {
@@ -2,5 +2,6 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
enum class Action { enum class Action {
START, START,
START_FOREGROUND,
STOP STOP
} }
@@ -4,10 +4,7 @@ import android.app.Service
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.IBinder import android.os.IBinder
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject
open class ForegroundService : Service() { open class ForegroundService : Service() {
@@ -25,7 +22,7 @@ open class ForegroundService : Service() {
val action = intent.action val action = intent.action
Timber.d("using an intent with action $action") Timber.d("using an intent with action $action")
when (action) { when (action) {
Action.START.name -> startService(intent.extras) Action.START.name, Action.START_FOREGROUND.name -> startService(intent.extras)
Action.STOP.name -> stopService(intent.extras) Action.STOP.name -> stopService(intent.extras)
"android.net.VpnService" -> { "android.net.VpnService" -> {
Timber.d("Always-on VPN starting service") Timber.d("Always-on VPN starting service")
@@ -0,0 +1,110 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.ActivityManager
import android.app.Service
import android.content.Context
import android.content.Context.ACTIVITY_SERVICE
import android.content.Intent
import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase
import com.zaneschepke.wireguardautotunnel.R
object ServiceManager {
@Suppress("DEPRECATION")
private // Deprecated for third party Services.
fun <T> Context.isServiceRunning(service: Class<T>) =
(getSystemService(ACTIVITY_SERVICE) as ActivityManager)
.getRunningServices(Integer.MAX_VALUE)
.any { it.service.className == service.name }
fun <T : Service> getServiceState(context: Context, cls : Class<T>): ServiceState {
val isServiceRunning = context.isServiceRunning(cls)
return if(isServiceRunning) ServiceState.STARTED else ServiceState.STOPPED
}
private fun <T : Service> actionOnService(action: Action, context: Context, cls : Class<T>, extras : Map<String,String>? = null) {
if (getServiceState(context, cls) == ServiceState.STOPPED && action == Action.STOP) return
if (getServiceState(context, cls) == ServiceState.STARTED && action == Action.START) return
val intent = Intent(context, cls).also {
it.action = action.name
extras?.forEach {(k, v) ->
it.putExtra(k, v)
}
}
intent.component?.javaClass
try {
when(action) {
Action.START_FOREGROUND -> {
context.startForegroundService(intent)
}
Action.START -> {
context.startService(intent)
}
Action.STOP -> context.startService(intent)
}
} catch (e : Exception) {
e.message?.let { Firebase.crashlytics.log(it) }
}
}
fun startVpnService(context : Context, tunnelConfig : String) {
actionOnService(
Action.START,
context,
WireGuardTunnelService::class.java,
mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig))
}
fun stopVpnService(context : Context) {
actionOnService(
Action.STOP,
context,
WireGuardTunnelService::class.java
)
}
fun startVpnServiceForeground(context : Context, tunnelConfig : String) {
actionOnService(
Action.START_FOREGROUND,
context,
WireGuardTunnelService::class.java,
mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig))
}
private fun startWatcherServiceForeground(context : Context, tunnelConfig : String) {
actionOnService(
Action.START, context,
WireGuardConnectivityWatcherService::class.java, mapOf(context.
getString(R.string.tunnel_extras_key) to
tunnelConfig))
}
fun startWatcherService(context : Context, tunnelConfig : String) {
actionOnService(
Action.START, context,
WireGuardConnectivityWatcherService::class.java, mapOf(context.
getString(R.string.tunnel_extras_key) to
tunnelConfig))
}
fun stopWatcherService(context : Context) {
actionOnService(
Action.STOP, context,
WireGuardConnectivityWatcherService::class.java)
}
fun toggleWatcherService(context: Context, tunnelConfig : String) {
when(getServiceState( context,
WireGuardConnectivityWatcherService::class.java,)) {
ServiceState.STARTED -> stopWatcherService(context)
ServiceState.STOPPED -> startWatcherService(context, tunnelConfig)
}
}
fun toggleWatcherServiceForeground(context: Context, tunnelConfig : String) {
when(getServiceState( context,
WireGuardConnectivityWatcherService::class.java,)) {
ServiceState.STARTED -> stopWatcherService(context)
ServiceState.STOPPED -> startWatcherServiceForeground(context, tunnelConfig)
}
}
}
@@ -1,56 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.ActivityManager
import android.app.Application
import android.app.Service
import android.content.Context
import android.content.Context.ACTIVITY_SERVICE
import android.content.Intent
import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase
object ServiceTracker {
@Suppress("DEPRECATION")
private // Deprecated for third party Services.
fun <T> Context.isServiceRunning(service: Class<T>) =
(getSystemService(ACTIVITY_SERVICE) as ActivityManager)
.getRunningServices(Integer.MAX_VALUE)
.any { it.service.className == service.name }
fun <T : Service> getServiceState(context: Context, cls : Class<T>): ServiceState {
val isServiceRunning = context.isServiceRunning(cls)
return if(isServiceRunning) ServiceState.STARTED else ServiceState.STOPPED
}
fun <T : Service> actionOnService(action: Action, application: Application, cls : Class<T>, extras : Map<String,String>? = null) {
if (getServiceState(application, cls) == ServiceState.STOPPED && action == Action.STOP) return
val intent = Intent(application, cls).also {
it.action = action.name
extras?.forEach {(k, v) ->
it.putExtra(k, v)
}
}
intent.component?.javaClass
try {
application.startService(intent)
} catch (e : Exception) {
e.message?.let { Firebase.crashlytics.log(it) }
}
}
fun <T : Service> actionOnService(action: Action, context: Context, cls : Class<T>, extras : Map<String,String>? = null) {
if (getServiceState(context, cls) == ServiceState.STOPPED && action == Action.STOP) return
val intent = Intent(context, cls).also {
it.action = action.name
extras?.forEach {(k, v) ->
it.putExtra(k, v)
}
}
intent.component?.javaClass
try {
context.startService(intent)
} catch (e : Exception) {
e.message?.let { Firebase.crashlytics.log(it) }
}
}
}
@@ -1,7 +1,6 @@
package com.zaneschepke.wireguardautotunnel.service.foreground package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.AlarmManager import android.app.AlarmManager
import android.app.Application
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@@ -54,23 +53,30 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
private lateinit var watcherJob : Job; private lateinit var watcherJob : Job;
private lateinit var setting : Settings private lateinit var setting : Settings
private lateinit var tunnelId: String private lateinit var tunnelConfig: String
private var wakeLock: PowerManager.WakeLock? = null private var wakeLock: PowerManager.WakeLock? = null
private val tag = this.javaClass.name; private val tag = this.javaClass.name;
override fun onCreate() {
super.onCreate()
CoroutineScope(Dispatchers.Main).launch {
launchWatcherNotification()
}
}
override fun startService(extras: Bundle?) { override fun startService(extras: Bundle?) {
super.startService(extras) super.startService(extras)
launchWatcherNotification()
val tunnelId = extras?.getString(getString(R.string.tunnel_extras_key)) val tunnelId = extras?.getString(getString(R.string.tunnel_extras_key))
if (tunnelId != null) { if (tunnelId != null) {
this.tunnelId = tunnelId this.tunnelConfig = tunnelId
} }
// we need this lock so our service gets not affected by Doze Mode // we need this lock so our service gets not affected by Doze Mode
initWakeLock() initWakeLock()
cancelWatcherJob() cancelWatcherJob()
launchWatcherNotification() if(this::tunnelConfig.isInitialized) {
if(this::tunnelId.isInitialized) {
startWatcherJob() startWatcherJob()
} else { } else {
stopService(extras) stopService(extras)
@@ -85,7 +91,6 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
} }
} }
cancelWatcherJob() cancelWatcherJob()
stopVPN()
stopSelf() stopSelf()
} }
@@ -183,41 +188,26 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
} }
private suspend fun manageVpn() { private suspend fun manageVpn() {
while(watcherJob.isActive) { while(true) {
if(setting.isTunnelOnMobileDataEnabled && if(setting.isTunnelOnMobileDataEnabled &&
!isWifiConnected && !isWifiConnected &&
isMobileDataConnected isMobileDataConnected
&& vpnService.getState() == Tunnel.State.DOWN) { && vpnService.getState() == Tunnel.State.DOWN) {
startVPN() ServiceManager.startVpnService(this, tunnelConfig)
} else if(!setting.isTunnelOnMobileDataEnabled && } else if(!setting.isTunnelOnMobileDataEnabled &&
!isWifiConnected && !isWifiConnected &&
vpnService.getState() == Tunnel.State.UP) { vpnService.getState() == Tunnel.State.UP) {
stopVPN() ServiceManager.stopVpnService(this)
} else if(isWifiConnected && } else if(isWifiConnected &&
!setting.trustedNetworkSSIDs.contains(currentNetworkSSID) && !setting.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
(vpnService.getState() != Tunnel.State.UP)) { (vpnService.getState() != Tunnel.State.UP)) {
startVPN() ServiceManager.startVpnService(this, tunnelConfig)
} else if((isWifiConnected && } else if((isWifiConnected &&
setting.trustedNetworkSSIDs.contains(currentNetworkSSID)) && setting.trustedNetworkSSIDs.contains(currentNetworkSSID)) &&
(vpnService.getState() == Tunnel.State.UP)) { (vpnService.getState() == Tunnel.State.UP)) {
stopVPN() ServiceManager.stopVpnService(this)
} }
delay(Constants.VPN_CONNECTIVITY_CHECK_INTERVAL) delay(Constants.VPN_CONNECTIVITY_CHECK_INTERVAL)
} }
} }
private fun startVPN() {
ServiceTracker.actionOnService(
Action.START,
this.applicationContext as Application,
WireGuardTunnelService::class.java,
mapOf(getString(R.string.tunnel_extras_key) to tunnelId))
}
private fun stopVPN() {
ServiceTracker.actionOnService(
Action.STOP,
this.applicationContext as Application,
WireGuardTunnelService::class.java
)
}
} }
@@ -37,8 +37,16 @@ class WireGuardTunnelService : ForegroundService() {
private var tunnelName : String = "" private var tunnelName : String = ""
override fun onCreate() {
super.onCreate()
CoroutineScope(Dispatchers.Main).launch {
launchVpnStartingNotification()
}
}
override fun startService(extras : Bundle?) { override fun startService(extras : Bundle?) {
super.startService(extras) super.startService(extras)
launchVpnStartingNotification()
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key)) val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
cancelJob() cancelJob()
job = CoroutineScope(Dispatchers.IO).launch { job = CoroutineScope(Dispatchers.IO).launch {
@@ -98,7 +106,7 @@ class WireGuardTunnelService : ForegroundService() {
override fun stopService(extras : Bundle?) { override fun stopService(extras : Bundle?) {
super.stopService(extras) super.stopService(extras)
CoroutineScope(Dispatchers.IO).launch() { CoroutineScope(Dispatchers.IO).launch {
vpnService.stopTunnel() vpnService.stopTunnel()
} }
cancelJob() cancelJob()
@@ -117,6 +125,18 @@ class WireGuardTunnelService : ForegroundService() {
super.startForeground(foregroundId, notification) super.startForeground(foregroundId, notification)
} }
private fun launchVpnStartingNotification() {
val notification = notificationService.createNotification(
channelId = getString(R.string.vpn_channel_id),
channelName = getString(R.string.vpn_channel_name),
title = getString(R.string.vpn_starting),
onGoing = false,
showTimestamp = true,
description = getString(R.string.attempt_connection)
)
super.startForeground(foregroundId, notification)
}
private fun launchVpnConnectionFailedNotification(message : String) { private fun launchVpnConnectionFailedNotification(message : String) {
val notification = notificationService.createNotification( val notification = notificationService.createNotification(
channelId = getString(R.string.vpn_channel_id), channelId = getString(R.string.vpn_channel_id),
@@ -28,7 +28,6 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Contex
object : ConnectivityManager.NetworkCallback( object : ConnectivityManager.NetworkCallback(
FLAG_INCLUDE_LOCATION_INFO FLAG_INCLUDE_LOCATION_INFO
) { ) {
override fun onAvailable(network: Network) { override fun onAvailable(network: Network) {
trySend(NetworkStatus.Available(network)) trySend(NetworkStatus.Available(network))
} }
@@ -68,6 +67,8 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Contex
} }
val request = NetworkRequest.Builder() val request = NetworkRequest.Builder()
.addTransportType(networkCapability) .addTransportType(networkCapability)
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
.build() .build()
connectivityManager.registerNetworkCallback(request, networkStatusCallback) connectivityManager.registerNetworkCallback(request, networkStatusCallback)
@@ -0,0 +1,53 @@
package com.zaneschepke.wireguardautotunnel.service.shortcut
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
class ShortcutsActivity : AppCompatActivity() {
@Inject
lateinit var settingsRepo : Repository<Settings>
private val scope = CoroutineScope(Dispatchers.Main);
private fun attemptWatcherServiceToggle(tunnelConfig : String) {
scope.launch {
val settings = settingsRepo.getAll()
if (!settings.isNullOrEmpty()) {
val setting = settings.first()
if(setting.isAutoTunnelEnabled) {
ServiceManager.toggleWatcherServiceForeground(this@ShortcutsActivity, tunnelConfig)
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if(intent.getStringExtra(ShortcutsManager.CLASS_NAME_EXTRA_KEY)
.equals(WireGuardTunnelService::class.java.name)) {
intent.getStringExtra(getString(R.string.tunnel_extras_key))?.let {
attemptWatcherServiceToggle(it)
}
when(intent.action){
Action.STOP.name -> ServiceManager.stopVpnService(this)
Action.START.name -> intent.getStringExtra(getString(R.string.tunnel_extras_key))
?.let { ServiceManager.startVpnService(this, it) }
}
}
finish()
}
}
@@ -0,0 +1,73 @@
package com.zaneschepke.wireguardautotunnel.service.shortcut
import android.content.Context
import android.content.Intent
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
object ShortcutsManager {
private const val SHORT_LABEL_MAX_SIZE = 10;
private const val LONG_LABEL_MAX_SIZE = 25;
private const val APPEND_ON = " On";
private const val APPEND_OFF = " Off"
const val CLASS_NAME_EXTRA_KEY = "className"
private fun createAndPushShortcut(context : Context, intent : Intent, id : String, shortLabel : String,
longLabel : String, drawable : Int ) {
val shortcut = ShortcutInfoCompat.Builder(context, id)
.setShortLabel(shortLabel)
.setLongLabel(longLabel)
.setIcon(IconCompat.createWithResource(context, drawable))
.setIntent(intent)
.build()
ShortcutManagerCompat.pushDynamicShortcut(context, shortcut)
}
fun createTunnelShortcuts(context : Context, tunnelConfig : TunnelConfig) {
createAndPushShortcut(context,
createTunnelOnIntent(context, mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig.toString())),
tunnelConfig.id.toString() + APPEND_ON,
tunnelConfig.name.take((SHORT_LABEL_MAX_SIZE - APPEND_ON.length)) + APPEND_ON,
tunnelConfig.name.take((LONG_LABEL_MAX_SIZE - APPEND_ON.length)) + APPEND_ON,
R.drawable.vpn_on
)
createAndPushShortcut(context,
createTunnelOffIntent(context, mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig.toString())),
tunnelConfig.id.toString() + APPEND_OFF,
tunnelConfig.name.take((SHORT_LABEL_MAX_SIZE - APPEND_OFF.length)) + APPEND_OFF,
tunnelConfig.name.take((LONG_LABEL_MAX_SIZE - APPEND_OFF.length)) + APPEND_OFF,
R.drawable.vpn_off
)
}
fun removeTunnelShortcuts(context : Context, tunnelConfig : TunnelConfig) {
ShortcutManagerCompat.removeDynamicShortcuts(context, listOf(tunnelConfig.id.toString() + APPEND_ON,
tunnelConfig.id.toString() + APPEND_OFF ))
}
private fun createTunnelOnIntent(context: Context, extras : Map<String,String>) : Intent {
return Intent(context, ShortcutsActivity::class.java).also {
it.action = Action.START.name
it.putExtra(CLASS_NAME_EXTRA_KEY, WireGuardTunnelService::class.java.name)
extras.forEach {(k, v) ->
it.putExtra(k, v)
}
}
}
private fun createTunnelOffIntent(context : Context, extras : Map<String,String>) : Intent {
return Intent(context, ShortcutsActivity::class.java).also {
it.action = Action.STOP.name
it.putExtra(CLASS_NAME_EXTRA_KEY, WireGuardTunnelService::class.java.name)
extras.forEach {(k, v) ->
it.putExtra(k, v)
}
}
}
}
@@ -0,0 +1,142 @@
package com.zaneschepke.wireguardautotunnel.service.tile
import android.os.Build
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class TunnelControlTile : TileService() {
@Inject
lateinit var settingsRepo : Repository<Settings>
@Inject
lateinit var configRepo : Repository<TunnelConfig>
@Inject
lateinit var vpnService : VpnService
private val scope = CoroutineScope(Dispatchers.Main);
private lateinit var job : Job
override fun onStartListening() {
job = scope.launch {
updateTileState()
}
super.onStartListening()
}
override fun onTileAdded() {
super.onTileAdded()
qsTile.contentDescription = this.resources.getString(R.string.toggle_vpn)
scope.launch {
updateTileState();
}
}
override fun onTileRemoved() {
super.onTileRemoved()
cancelJob()
}
override fun onClick() {
super.onClick()
unlockAndRun {
scope.launch {
try {
val tunnel = determineTileTunnel();
if(tunnel != null) {
attemptWatcherServiceToggle(tunnel.toString())
if(vpnService.getState() == Tunnel.State.UP) {
ServiceManager.stopVpnService(this@TunnelControlTile)
} else {
ServiceManager.startVpnServiceForeground(this@TunnelControlTile, tunnel.toString())
}
}
} catch (e : Exception) {
Timber.e(e.message)
} finally {
cancel()
}
}
}
}
private suspend fun determineTileTunnel() : TunnelConfig? {
var tunnelConfig : TunnelConfig? = null;
val settings = settingsRepo.getAll()
if (!settings.isNullOrEmpty()) {
val setting = settings.first()
tunnelConfig = if (setting.defaultTunnel != null) {
TunnelConfig.from(setting.defaultTunnel!!);
} else {
val config = configRepo.getAll()?.first();
config;
}
}
return tunnelConfig;
}
private fun attemptWatcherServiceToggle(tunnelConfig : String) {
scope.launch {
val settings = settingsRepo.getAll()
if (!settings.isNullOrEmpty()) {
val setting = settings.first()
if(setting.isAutoTunnelEnabled) {
ServiceManager.toggleWatcherServiceForeground(this@TunnelControlTile, tunnelConfig)
}
}
}
}
private suspend fun updateTileState() {
vpnService.state.collect {
when(it) {
Tunnel.State.UP -> {
qsTile.state = Tile.STATE_ACTIVE
}
Tunnel.State.DOWN -> {
qsTile.state = Tile.STATE_INACTIVE;
}
else -> {
qsTile.state = Tile.STATE_UNAVAILABLE
}
}
val config = determineTileTunnel();
setTileDescription(config?.name ?: this.resources.getString(R.string.no_tunnel_available))
qsTile.updateTile()
}
}
private fun setTileDescription(description : String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
qsTile.subtitle = description
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
qsTile.stateDescription = description;
}
}
private fun cancelJob() {
if(this::job.isInitialized) {
job.cancel();
}
}
}
@@ -30,6 +30,7 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend,
override val tunnelName get() = _tunnelName.asStateFlow() override val tunnelName get() = _tunnelName.asStateFlow()
private val _state = MutableSharedFlow<Tunnel.State>( private val _state = MutableSharedFlow<Tunnel.State>(
onBufferOverflow = BufferOverflow.DROP_OLDEST,
replay = 1) replay = 1)
private val _handshakeStatus = MutableSharedFlow<HandshakeStatus>(replay = 1, private val _handshakeStatus = MutableSharedFlow<HandshakeStatus>(replay = 1,
@@ -11,5 +11,5 @@ data class Settings(
var isTunnelOnMobileDataEnabled : Boolean = false, var isTunnelOnMobileDataEnabled : Boolean = false,
var trustedNetworkSSIDs : MutableList<String> = mutableListOf(), var trustedNetworkSSIDs : MutableList<String> = mutableListOf(),
var defaultTunnel : String? = null, var defaultTunnel : String? = null,
var isAlwaysOnVpnEnabled : Boolean = false var isAlwaysOnVpnEnabled : Boolean = false,
) )
@@ -152,7 +152,7 @@ class MainActivity : AppCompatActivity() {
} }
} }
}) { }) {
MainScreen(padding = padding, snackbarHostState = snackbarHostState, navController = navController, focusRequester = focusRequester) MainScreen(padding = padding, snackbarHostState = snackbarHostState, navController = navController)
} }
composable(Routes.Settings.name, enterTransition = { composable(Routes.Settings.name, enterTransition = {
when (initialState.destination.route) { when (initialState.destination.route) {
@@ -0,0 +1,80 @@
package com.zaneschepke.wireguardautotunnel.ui.common
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Clear
import androidx.compose.material.icons.rounded.Search
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import com.zaneschepke.wireguardautotunnel.R
@Composable
fun SearchBar(
onQuery : (queryString : String) -> Unit
) {
// Immediately update and keep track of query from text field changes.
var query: String by rememberSaveable { mutableStateOf("") }
var showClearIcon by rememberSaveable { mutableStateOf(false) }
if (query.isEmpty()) {
showClearIcon = false
} else if (query.isNotEmpty()) {
showClearIcon = true
}
TextField(
value = query,
onValueChange = { onQueryChanged ->
// If user makes changes to text, immediately updated it.
query = onQueryChanged
onQuery(onQueryChanged)
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Search,
tint = MaterialTheme.colorScheme.onBackground,
contentDescription = stringResource(id = R.string.search_icon)
)
},
trailingIcon = {
if (showClearIcon) {
IconButton(onClick = { query = "" }) {
Icon(
imageVector = Icons.Rounded.Clear,
tint = MaterialTheme.colorScheme.onBackground,
contentDescription = stringResource(id = R.string.clear_icon)
)
}
}
},
maxLines = 1,
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
disabledContainerColor = Color.Transparent,
),
placeholder = { Text(text = stringResource(R.string.hint_search_packages)) },
textStyle = MaterialTheme.typography.bodySmall,
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
modifier = Modifier
.fillMaxWidth()
.background(color = MaterialTheme.colorScheme.background, shape = RectangleShape)
)
}
@@ -1,6 +1,5 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.config package com.zaneschepke.wireguardautotunnel.ui.screens.config
import android.content.pm.PackageManager
import android.widget.Toast import android.widget.Toast
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -25,7 +24,6 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
@@ -45,6 +43,7 @@ import androidx.navigation.NavController
import com.google.accompanist.drawablepainter.DrawablePainter import com.google.accompanist.drawablepainter.DrawablePainter
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.Routes import com.zaneschepke.wireguardautotunnel.ui.Routes
import com.zaneschepke.wireguardautotunnel.ui.common.SearchBar
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(ExperimentalComposeUiApi::class) @OptIn(ExperimentalComposeUiApi::class)
@@ -71,7 +70,7 @@ fun ConfigScreen(
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.getTunnelById(id) viewModel.getTunnelById(id)
viewModel.emitAllInternetCapablePackages() viewModel.emitQueriedPackages("")
viewModel.emitCurrentPackageConfigurations(id) viewModel.emitCurrentPackageConfigurations(id)
} }
@@ -165,6 +164,16 @@ fun ConfigScreen(
} }
} }
} }
item {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween) {
SearchBar(viewModel::emitQueriedPackages);
}
}
items(packages) { pack -> items(packages) { pack ->
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@@ -8,12 +8,15 @@ import android.os.Build
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.toMutableStateList import androidx.compose.runtime.toMutableStateList
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.zaneschepke.wireguardautotunnel.repository.Repository import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutsManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@@ -96,8 +99,12 @@ class ConfigViewModel @Inject constructor(private val application : Application,
} }
} }
suspend fun emitAllInternetCapablePackages() { fun emitQueriedPackages(query : String) {
_packages.emit(getAllInternetCapablePackages()) viewModelScope.launch {
_packages.emit(getAllInternetCapablePackages().filter {
it.packageName.contains(query)
})
}
} }
private fun getAllInternetCapablePackages() : List<PackageInfo> { private fun getAllInternetCapablePackages() : List<PackageInfo> {
@@ -108,12 +115,14 @@ class ConfigViewModel @Inject constructor(private val application : Application,
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageManager.getPackagesHoldingPermissions(permissions, PackageManager.PackageInfoFlags.of(0L)) packageManager.getPackagesHoldingPermissions(permissions, PackageManager.PackageInfoFlags.of(0L))
} else { } else {
@Suppress("DEPRECATION")
packageManager.getPackagesHoldingPermissions(permissions, 0) packageManager.getPackagesHoldingPermissions(permissions, 0)
} }
} }
suspend fun onSaveAllChanges() { suspend fun onSaveAllChanges() {
if(_tunnel.value != null) {
ShortcutsManager.removeTunnelShortcuts(application, _tunnel.value!!)
}
var wgQuick = _tunnel.value?.wgQuick var wgQuick = _tunnel.value?.wgQuick
if(wgQuick != null) { if(wgQuick != null) {
wgQuick = if(_include.value) { wgQuick = if(_include.value) {
@@ -129,6 +138,7 @@ class ConfigViewModel @Inject constructor(private val application : Application,
wgQuick = wgQuick wgQuick = wgQuick
)?.let { )?.let {
tunnelRepo.save(it) tunnelRepo.save(it)
ShortcutsManager.createTunnelShortcuts(application, it)
val settings = settingsRepo.getAll() val settings = settingsRepo.getAll()
if(settings != null) { if(settings != null) {
val setting = settings[0] val setting = settings[0]
@@ -1,6 +1,7 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main package com.zaneschepke.wireguardautotunnel.ui.screens.main
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
@@ -17,7 +18,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FileOpen import androidx.compose.material.icons.filled.FileOpen
@@ -85,7 +86,6 @@ import kotlinx.coroutines.launch
@Composable @Composable
fun MainScreen( fun MainScreen(
viewModel: MainViewModel = hiltViewModel(), padding: PaddingValues, viewModel: MainViewModel = hiltViewModel(), padding: PaddingValues,
focusRequester: FocusRequester,
snackbarHostState: SnackbarHostState, navController: NavController snackbarHostState: SnackbarHostState, navController: NavController
) { ) {
@@ -103,6 +103,7 @@ fun MainScreen(
val state by viewModel.state.collectAsStateWithLifecycle(Tunnel.State.DOWN) val state by viewModel.state.collectAsStateWithLifecycle(Tunnel.State.DOWN)
val tunnelName by viewModel.tunnelName.collectAsStateWithLifecycle("") val tunnelName by viewModel.tunnelName.collectAsStateWithLifecycle("")
// Nested scroll for control FAB // Nested scroll for control FAB
val nestedScrollConnection = remember { val nestedScrollConnection = remember {
object : NestedScrollConnection { object : NestedScrollConnection {
@@ -135,11 +136,9 @@ fun MainScreen(
} }
val pickFileLauncher = rememberLauncherForActivityResult( val pickFileLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.GetContent() ActivityResultContracts.StartActivityForResult()
) { file -> ) { result ->
if (file != null) { result.data?.data?.let { viewModel.onTunnelFileSelected(it) }
viewModel.onTunnelFileSelected(file)
}
} }
Scaffold( Scaffold(
@@ -149,7 +148,7 @@ fun MainScreen(
}) })
}, },
floatingActionButtonPosition = FabPosition.End, floatingActionButtonPosition = FabPosition.End,
floatingActionButton = { floatingActionButton = {
AnimatedVisibility( AnimatedVisibility(
visible = isVisible.value, visible = isVisible.value,
enter = slideInVertically(initialOffsetY = { it * 2 }), enter = slideInVertically(initialOffsetY = { it * 2 }),
@@ -196,7 +195,11 @@ fun MainScreen(
.fillMaxWidth() .fillMaxWidth()
.clickable { .clickable {
showBottomSheet = false showBottomSheet = false
pickFileLauncher.launch("*/*") val fileSelectionIntent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
}
pickFileLauncher.launch(fileSelectionIntent)
} }
.padding(10.dp) .padding(10.dp)
) { ) {
@@ -241,9 +244,13 @@ fun MainScreen(
.padding(padding) .padding(padding)
) { ) {
LazyColumn(modifier = Modifier.fillMaxSize() LazyColumn(
.nestedScroll(nestedScrollConnection),) { modifier = Modifier
items(tunnels.toList()) { tunnel -> .fillMaxSize()
.nestedScroll(nestedScrollConnection),
) {
itemsIndexed(tunnels.toList()) { _, tunnel ->
val focusRequester = FocusRequester();
RowListItem(leadingIcon = Icons.Rounded.Circle, RowListItem(leadingIcon = Icons.Rounded.Circle,
leadingIconColor = if (tunnelName == tunnel.name) when (handshakeStatus) { leadingIconColor = if (tunnelName == tunnel.name) when (handshakeStatus) {
HandshakeStatus.HEALTHY -> mint HandshakeStatus.HEALTHY -> mint
@@ -263,15 +270,15 @@ fun MainScreen(
selectedTunnel = tunnel; selectedTunnel = tunnel;
}, },
onClick = { onClick = {
if(!WireGuardAutoTunnel.isRunningOnAndroidTv(context)){ if (!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
navController.navigate("${Routes.Detail.name}/${tunnel.id}") navController.navigate("${Routes.Detail.name}/${tunnel.id}")
} else { } else {
focusRequester.requestFocus() focusRequester.requestFocus()
} }
}, },
rowButton = { rowButton = {
if (tunnel.id == selectedTunnel?.id) { if (tunnel.id == selectedTunnel?.id) {
Row() { Row {
IconButton(onClick = { IconButton(onClick = {
navController.navigate("${Routes.Config.name}/${selectedTunnel?.id}") navController.navigate("${Routes.Config.name}/${selectedTunnel?.id}")
}) { }) {
@@ -287,30 +294,43 @@ fun MainScreen(
} }
} }
} else { } else {
if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)){ if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
Row() { Row {
IconButton(modifier = Modifier.focusRequester(focusRequester),onClick = { IconButton(
navController.navigate("${Routes.Detail.name}/${tunnel.id}") modifier = Modifier.focusRequester(focusRequester),
}) { onClick = {
navController.navigate("${Routes.Detail.name}/${tunnel.id}")
}) {
Icon(Icons.Rounded.Info, "Info") Icon(Icons.Rounded.Info, "Info")
} }
IconButton(onClick = { IconButton(onClick = {
if (state == Tunnel.State.UP && tunnel.name == tunnelName) if (state == Tunnel.State.UP && tunnel.name == tunnelName)
scope.launch { scope.launch {
viewModel.showSnackBarMessage(context.resources.getString(R.string.turn_off_tunnel)) viewModel.showSnackBarMessage(
context.resources.getString(
R.string.turn_off_tunnel
)
)
} else { } else {
navController.navigate("${Routes.Config.name}/${tunnel.id}") navController.navigate("${Routes.Config.name}/${tunnel.id}")
} }
}) { }) {
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit)) Icon(
Icons.Rounded.Edit,
stringResource(id = R.string.edit)
)
} }
IconButton(onClick = { IconButton(onClick = {
if (state == Tunnel.State.UP && tunnel.name == tunnelName) if (state == Tunnel.State.UP && tunnel.name == tunnelName)
scope.launch { scope.launch {
viewModel.showSnackBarMessage(context.resources.getString(R.string.turn_off_tunnel)) viewModel.showSnackBarMessage(
context.resources.getString(
R.string.turn_off_tunnel
)
)
} else { } else {
viewModel.onDelete(tunnel) viewModel.onDelete(tunnel)
} }
}) { }) {
Icon( Icon(
Icons.Rounded.Delete, Icons.Rounded.Delete,
@@ -12,11 +12,10 @@ import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.repository.Repository import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.barcode.CodeScanner import com.zaneschepke.wireguardautotunnel.service.barcode.CodeScanner
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceState import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceState
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceTracker import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService import com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutsManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
@@ -64,23 +63,17 @@ class MainViewModel @Inject constructor(private val application : Application,
} }
private fun validateWatcherServiceState(settings: Settings) { private fun validateWatcherServiceState(settings: Settings) {
val watcherState = ServiceTracker.getServiceState(application, WireGuardConnectivityWatcherService::class.java) val watcherState = ServiceManager.getServiceState(application.applicationContext, WireGuardConnectivityWatcherService::class.java)
if(settings.isAutoTunnelEnabled && watcherState == ServiceState.STOPPED && settings.defaultTunnel != null) { if(settings.isAutoTunnelEnabled && watcherState == ServiceState.STOPPED && settings.defaultTunnel != null) {
startWatcherService(settings.defaultTunnel!!) ServiceManager.startWatcherService(application.applicationContext, settings.defaultTunnel!!)
} }
} }
private fun startWatcherService(tunnel : String) {
ServiceTracker.actionOnService(
Action.START, application,
WireGuardConnectivityWatcherService::class.java,
mapOf(application.resources.getString(R.string.tunnel_extras_key) to tunnel))
}
fun onDelete(tunnel : TunnelConfig) { fun onDelete(tunnel : TunnelConfig) {
viewModelScope.launch { viewModelScope.launch {
if(tunnelRepo.count() == 1L) { if(tunnelRepo.count() == 1L) {
ServiceTracker.actionOnService( Action.STOP, application, WireGuardConnectivityWatcherService::class.java) ServiceManager.stopWatcherService(application.applicationContext)
val settings = settingsRepo.getAll() val settings = settingsRepo.getAll()
if(!settings.isNullOrEmpty()) { if(!settings.isNullOrEmpty()) {
val setting = settings[0] val setting = settings[0]
@@ -91,22 +84,23 @@ class MainViewModel @Inject constructor(private val application : Application,
} }
} }
tunnelRepo.delete(tunnel) tunnelRepo.delete(tunnel)
ShortcutsManager.removeTunnelShortcuts(application.applicationContext, tunnel)
} }
} }
fun onTunnelStart(tunnelConfig : TunnelConfig) = viewModelScope.launch { fun onTunnelStart(tunnelConfig : TunnelConfig) = viewModelScope.launch {
ServiceTracker.actionOnService( Action.START, application, WireGuardTunnelService::class.java, ServiceManager.startVpnService(application.applicationContext, tunnelConfig.toString())
mapOf(application.resources.getString(R.string.tunnel_extras_key) to tunnelConfig.toString()))
} }
fun onTunnelStop() { fun onTunnelStop() {
ServiceTracker.actionOnService( Action.STOP, application, WireGuardTunnelService::class.java) ServiceManager.stopVpnService(application.applicationContext)
} }
suspend fun onTunnelQRSelected() { suspend fun onTunnelQRSelected() {
codeScanner.scan().collect { codeScanner.scan().collect {
if(!it.isNullOrEmpty() && it.contains(application.resources.getString(R.string.config_validation))) { if(!it.isNullOrEmpty() && it.contains(application.resources.getString(R.string.config_validation))) {
tunnelRepo.save(TunnelConfig(name = defaultConfigName(), wgQuick = it)) val tunnelConfig = TunnelConfig(name = defaultConfigName(), wgQuick = it)
saveTunnel(tunnelConfig)
} else if(!it.isNullOrEmpty() && it.contains(application.resources.getString(R.string.barcode_downloading))) { } else if(!it.isNullOrEmpty() && it.contains(application.resources.getString(R.string.barcode_downloading))) {
showSnackBarMessage(application.resources.getString(R.string.barcode_downloading_message)) showSnackBarMessage(application.resources.getString(R.string.barcode_downloading_message))
} else { } else {
@@ -130,9 +124,7 @@ class MainViewModel @Inject constructor(private val application : Application,
val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8) val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8)
val config = Config.parse(bufferReader) val config = Config.parse(bufferReader)
val tunnelName = getNameFromFileName(fileName) val tunnelName = getNameFromFileName(fileName)
viewModelScope.launch { saveTunnel(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString()))
tunnelRepo.save(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString()))
}
stream.close() stream.close()
} catch(_: BadConfigException) { } catch(_: BadConfigException) {
viewModelScope.launch { viewModelScope.launch {
@@ -141,10 +133,22 @@ class MainViewModel @Inject constructor(private val application : Application,
} }
} }
private fun saveTunnel(tunnelConfig : TunnelConfig) {
viewModelScope.launch {
tunnelRepo.save(tunnelConfig)
ShortcutsManager.createTunnelShortcuts(application.applicationContext, tunnelConfig)
}
}
@SuppressLint("Range") @SuppressLint("Range")
private fun getFileName(context: Context, uri: Uri): String { private fun getFileName(context: Context, uri: Uri): String {
if (uri.scheme == "content") { if (uri.scheme == "content") {
val cursor = context.contentResolver.query(uri, null, null, null, null) val cursor = try {
context.contentResolver.query(uri, null, null, null, null)
} catch (e : Exception) {
Timber.d("Exception getting config name")
null
}
cursor ?: return defaultConfigName() cursor ?: return defaultConfigName()
cursor.use { cursor.use {
if(cursor.moveToFirst()) { if(cursor.moveToFirst()) {
@@ -96,8 +96,6 @@ fun SettingsScreen(
val settings by viewModel.settings.collectAsStateWithLifecycle() val settings by viewModel.settings.collectAsStateWithLifecycle()
val trustedSSIDs by viewModel.trustedSSIDs.collectAsStateWithLifecycle() val trustedSSIDs by viewModel.trustedSSIDs.collectAsStateWithLifecycle()
val tunnels by viewModel.tunnels.collectAsStateWithLifecycle(mutableListOf()) val tunnels by viewModel.tunnels.collectAsStateWithLifecycle(mutableListOf())
val backgroundLocationState =
rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION) val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
var currentText by remember { mutableStateOf("") } var currentText by remember { mutableStateOf("") }
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
@@ -135,41 +133,44 @@ fun SettingsScreen(
context.startActivity(intentSettings) context.startActivity(intentSettings)
} }
} }
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if(!backgroundLocationState.status.isGranted && Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { val backgroundLocationState =
Column(horizontalAlignment = Alignment.CenterHorizontally, rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
verticalArrangement = Arrangement.Top, if(!backgroundLocationState.status.isGranted) {
modifier = Modifier Column(horizontalAlignment = Alignment.CenterHorizontally,
.fillMaxSize() verticalArrangement = Arrangement.Top,
.verticalScroll(scrollState) modifier = Modifier
.padding(padding)) { .fillMaxSize()
Icon(Icons.Rounded.LocationOff, contentDescription = stringResource(id = R.string.map), modifier = Modifier .verticalScroll(scrollState)
.padding(30.dp) .padding(padding)) {
.size(128.dp)) Icon(Icons.Rounded.LocationOff, contentDescription = stringResource(id = R.string.map), modifier = Modifier
Text(stringResource(R.string.prominent_background_location_title), textAlign = TextAlign.Center, modifier = Modifier.padding(30.dp), fontSize = 20.sp) .padding(30.dp)
Text(stringResource(R.string.prominent_background_location_message), textAlign = TextAlign.Center, modifier = Modifier.padding(30.dp), fontSize = 15.sp) .size(128.dp))
Row( Text(stringResource(R.string.prominent_background_location_title), textAlign = TextAlign.Center, modifier = Modifier.padding(30.dp), fontSize = 20.sp)
modifier = if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) Modifier Text(stringResource(R.string.prominent_background_location_message), textAlign = TextAlign.Center, modifier = Modifier.padding(30.dp), fontSize = 15.sp)
.fillMaxWidth() Row(
.padding(10.dp) else Modifier modifier = if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(30.dp), .padding(10.dp) else Modifier
verticalAlignment = Alignment.CenterVertically, .fillMaxWidth()
horizontalArrangement = Arrangement.SpaceEvenly .padding(30.dp),
) { verticalAlignment = Alignment.CenterVertically,
Button(onClick = { horizontalArrangement = Arrangement.SpaceEvenly
navController.navigate(Routes.Main.name) ) {
}) { Button(onClick = {
Text(stringResource(id = R.string.no_thanks)) navController.navigate(Routes.Main.name)
} }) {
Button(modifier = Modifier.focusRequester(focusRequester), onClick = { Text(stringResource(id = R.string.no_thanks))
openSettings() }
}) { Button(modifier = Modifier.focusRequester(focusRequester), onClick = {
Text(stringResource(id = R.string.turn_on)) openSettings()
}) {
Text(stringResource(id = R.string.turn_on))
}
} }
} }
return
} }
return
} }
if(!fineLocationState.status.isGranted) { if(!fineLocationState.status.isGranted) {
@@ -7,9 +7,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.repository.Repository import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.foreground.Action import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceTracker
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.ui.ViewState import com.zaneschepke.wireguardautotunnel.ui.ViewState
@@ -77,32 +75,18 @@ class SettingsViewModel @Inject constructor(private val application : Applicatio
return return
} }
if(_settings.value.isAutoTunnelEnabled) { if(_settings.value.isAutoTunnelEnabled) {
actionOnWatcherService(Action.STOP) ServiceManager.stopWatcherService(application)
} else { } else {
actionOnWatcherService(Action.START) if(_settings.value.defaultTunnel != null) {
val defaultTunnel = _settings.value.defaultTunnel
ServiceManager.startWatcherService(application, defaultTunnel!!)
}
} }
settingsRepo.save(_settings.value.copy( settingsRepo.save(_settings.value.copy(
isAutoTunnelEnabled = !_settings.value.isAutoTunnelEnabled isAutoTunnelEnabled = !_settings.value.isAutoTunnelEnabled
)) ))
} }
private fun actionOnWatcherService(action : Action) {
when(action) {
Action.START -> {
if(_settings.value.defaultTunnel != null) {
val defaultTunnel = _settings.value.defaultTunnel
ServiceTracker.actionOnService(
action, application,
WireGuardConnectivityWatcherService::class.java,
mapOf(application.resources.getString(R.string.tunnel_extras_key) to defaultTunnel.toString()))
}
}
Action.STOP -> {
ServiceTracker.actionOnService( Action.STOP, application,
WireGuardConnectivityWatcherService::class.java)
}
}
}
suspend fun showSnackBarMessage(message : String) { suspend fun showSnackBarMessage(message : String) {
_viewState.emit(_viewState.value.copy( _viewState.emit(_viewState.value.copy(
showSnackbarMessage = true, showSnackbarMessage = true,
+5
View File
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,1L3,5v6c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12V5l-9,-4z"/>
</vector>
+5
View File
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M20.83,18H21v-4h2v-4H12.83L20.83,18zM19.78,22.61l1.41,-1.41L2.81,2.81L1.39,4.22l2.59,2.59C2.2,7.85 1,9.79 1,12c0,3.31 2.69,6 6,6c2.21,0 4.15,-1.2 5.18,-2.99L19.78,22.61zM8.99,11.82C9,11.88 9,11.94 9,12c0,1.1 -0.9,2 -2,2s-2,-0.9 -2,-2s0.9,-2 2,-2c0.06,0 0.12,0 0.18,0.01L8.99,11.82z"/>
</vector>
+5
View File
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12.65,10C11.83,7.67 9.61,6 7,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6c2.61,0 4.83,-1.67 5.65,-4H17v4h4v-4h2v-4H12.65zM7,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z"/>
</vector>
+7
View File
@@ -84,4 +84,11 @@
<string name="detecting_location_services_disabled">Detecting Location Services disabled</string> <string name="detecting_location_services_disabled">Detecting Location Services disabled</string>
<string name="precise_location_message">This feature requires precise location to access Wi-Fi SSID name. Please enable precise location here or in the app settings.</string> <string name="precise_location_message">This feature requires precise location to access Wi-Fi SSID name. Please enable precise location here or in the app settings.</string>
<string name="request">Request</string> <string name="request">Request</string>
<string name="toggle_vpn">Toggle VPN</string>
<string name="no_tunnel_available">No tunnels available</string>
<string name="hint_search_packages">Search packages</string>
<string name="clear_icon">Clear Icon</string>
<string name="search_icon">Search Icon</string>
<string name="attempt_connection">Attempting connection..</string>
<string name="vpn_starting">VPN Starting</string>
</resources> </resources>
+5 -5
View File
@@ -1,20 +1,20 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
val objectBoxVersion by extra("3.5.1") val objectBoxVersion by extra("3.7.0")
val hiltVersion by extra("2.47") val hiltVersion by extra("2.48")
val accompanistVersion by extra("0.31.2-alpha") val accompanistVersion by extra("0.31.2-alpha")
dependencies { dependencies {
classpath("io.objectbox:objectbox-gradle-plugin:$objectBoxVersion") classpath("io.objectbox:objectbox-gradle-plugin:$objectBoxVersion")
classpath("com.google.gms:google-services:4.3.15") classpath("com.google.gms:google-services:4.3.15")
classpath("com.google.firebase:firebase-crashlytics-gradle:2.9.7") classpath("com.google.firebase:firebase-crashlytics-gradle:2.9.9")
} }
} }
plugins { plugins {
id("com.android.application") version "8.2.0-alpha15" apply false id("com.android.application") version "8.2.0-beta03" apply false
id("org.jetbrains.kotlin.android") version "1.8.22" apply false id("org.jetbrains.kotlin.android") version "1.8.22" apply false
id("com.google.dagger.hilt.android") version "2.44" apply false id("com.google.dagger.hilt.android") version "2.48" apply false
kotlin("plugin.serialization") version "1.8.22" apply false kotlin("plugin.serialization") version "1.8.22" apply false
} }