feat: tunnel on ethernet
Adds settings feature to enable auto tunneling on ethernet connections. Add alphabetical sorting for split tunneling packages Fix bug where search was not searching by displayed label but by package name Other refactoring and improvements. Closes #29 Closes #27 Closes #28
This commit is contained in:
parent
1714618f0c
commit
7fbc51af4c
|
@ -14,8 +14,8 @@ android {
|
||||||
applicationId = "com.zaneschepke.wireguardautotunnel"
|
applicationId = "com.zaneschepke.wireguardautotunnel"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 30000
|
versionCode = 30001
|
||||||
versionName = "3.0.0"
|
versionName = "3.0.1"
|
||||||
|
|
||||||
multiDexEnabled = true
|
multiDexEnabled = true
|
||||||
|
|
||||||
|
|
|
@ -56,6 +56,7 @@
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
|
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
|
@ -117,8 +118,5 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
<receiver android:exported="false" android:name=".receiver.NotificationActionReceiver"/>
|
<receiver android:exported="false" android:name=".receiver.NotificationActionReceiver"/>
|
||||||
<meta-data
|
|
||||||
android:name="com.google.mlkit.vision.DEPENDENCIES"
|
|
||||||
android:value="barcode_ui"/>
|
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
|
@ -4,8 +4,12 @@ object Constants {
|
||||||
const val VPN_CONNECTIVITY_CHECK_INTERVAL = 3000L
|
const val VPN_CONNECTIVITY_CHECK_INTERVAL = 3000L
|
||||||
const val VPN_STATISTIC_CHECK_INTERVAL = 10000L
|
const val VPN_STATISTIC_CHECK_INTERVAL = 10000L
|
||||||
const val SNACKBAR_DELAY = 3000L
|
const val SNACKBAR_DELAY = 3000L
|
||||||
const val TOGGLE_TUNNEL_DELAY = 1000L
|
const val TOGGLE_TUNNEL_DELAY = 500L
|
||||||
const val FADE_IN_ANIMATION_DURATION = 1000
|
const val FADE_IN_ANIMATION_DURATION = 1000
|
||||||
const val SLIDE_IN_ANIMATION_DURATION = 500
|
const val SLIDE_IN_ANIMATION_DURATION = 500
|
||||||
const val SLIDE_IN_TRANSITION_OFFSET = 1000
|
const val SLIDE_IN_TRANSITION_OFFSET = 1000
|
||||||
|
const val VALID_FILE_EXTENSION = ".conf"
|
||||||
|
const val URI_CONTENT_SCHEME = "content"
|
||||||
|
const val URI_PACKAGE_SCHEME = "package"
|
||||||
|
const val ALLOWED_FILE_TYPES = "*/*"
|
||||||
}
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.module
|
package com.zaneschepke.wireguardautotunnel.module
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
|
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
|
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
|
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
|
||||||
|
@ -26,4 +27,8 @@ abstract class ServiceModule {
|
||||||
@Binds
|
@Binds
|
||||||
@ServiceScoped
|
@ServiceScoped
|
||||||
abstract fun provideMobileDataService(mobileDataService : MobileDataService) : NetworkService<MobileDataService>
|
abstract fun provideMobileDataService(mobileDataService : MobileDataService) : NetworkService<MobileDataService>
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@ServiceScoped
|
||||||
|
abstract fun provideEthernetService(ethernetService: EthernetService) : NetworkService<EthernetService>
|
||||||
}
|
}
|
|
@ -23,7 +23,7 @@ class BootReceiver : BroadcastReceiver() {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
try {
|
try {
|
||||||
val settings = settingsRepo.getAll()
|
val settings = settingsRepo.getAll()
|
||||||
if (!settings.isNullOrEmpty()) {
|
if (settings.isNotEmpty()) {
|
||||||
val setting = settings.first()
|
val setting = settings.first()
|
||||||
if (setting.isAutoTunnelEnabled && setting.defaultTunnel != null) {
|
if (setting.isAutoTunnelEnabled && setting.defaultTunnel != null) {
|
||||||
ServiceManager.startWatcherService(context, setting.defaultTunnel!!)
|
ServiceManager.startWatcherService(context, setting.defaultTunnel!!)
|
||||||
|
|
|
@ -12,4 +12,5 @@ data class Settings(
|
||||||
@ColumnInfo(name = "trusted_network_ssids") var trustedNetworkSSIDs : MutableList<String> = mutableListOf(),
|
@ColumnInfo(name = "trusted_network_ssids") var trustedNetworkSSIDs : MutableList<String> = mutableListOf(),
|
||||||
@ColumnInfo(name = "default_tunnel") var defaultTunnel : String? = null,
|
@ColumnInfo(name = "default_tunnel") var defaultTunnel : String? = null,
|
||||||
@ColumnInfo(name = "is_always_on_vpn_enabled") var isAlwaysOnVpnEnabled : Boolean = false,
|
@ColumnInfo(name = "is_always_on_vpn_enabled") var isAlwaysOnVpnEnabled : Boolean = false,
|
||||||
|
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled") var isTunnelOnEthernetEnabled : Boolean = false,
|
||||||
)
|
)
|
|
@ -12,6 +12,7 @@ import com.zaneschepke.wireguardautotunnel.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
|
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
|
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus
|
import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus
|
||||||
|
@ -38,6 +39,9 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var mobileDataService : NetworkService<MobileDataService>
|
lateinit var mobileDataService : NetworkService<MobileDataService>
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var ethernetService: NetworkService<EthernetService>
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var settingsRepo: SettingsDoa
|
lateinit var settingsRepo: SettingsDoa
|
||||||
|
|
||||||
|
@ -48,6 +52,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
lateinit var vpnService : VpnService
|
lateinit var vpnService : VpnService
|
||||||
|
|
||||||
private var isWifiConnected = false;
|
private var isWifiConnected = false;
|
||||||
|
private var isEthernetConnected = false;
|
||||||
private var isMobileDataConnected = false;
|
private var isMobileDataConnected = false;
|
||||||
private var currentNetworkSSID = "";
|
private var currentNetworkSSID = "";
|
||||||
|
|
||||||
|
@ -142,6 +147,11 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
watchForMobileDataConnectivityChanges()
|
watchForMobileDataConnectivityChanges()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if(setting.isTunnelOnEthernetEnabled) {
|
||||||
|
launch {
|
||||||
|
watchForEthernetConnectivityChanges()
|
||||||
|
}
|
||||||
|
}
|
||||||
launch {
|
launch {
|
||||||
manageVpn()
|
manageVpn()
|
||||||
}
|
}
|
||||||
|
@ -167,6 +177,25 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun watchForEthernetConnectivityChanges() {
|
||||||
|
ethernetService.networkStatus.collect {
|
||||||
|
when (it) {
|
||||||
|
is NetworkStatus.Available -> {
|
||||||
|
Timber.d("Gained Ethernet connection")
|
||||||
|
isEthernetConnected = true
|
||||||
|
}
|
||||||
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
|
Timber.d("Ethernet capabilities changed")
|
||||||
|
isEthernetConnected = true
|
||||||
|
}
|
||||||
|
is NetworkStatus.Unavailable -> {
|
||||||
|
isEthernetConnected = false
|
||||||
|
Timber.d("Lost Ethernet connection")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun watchForWifiConnectivityChanges() {
|
private suspend fun watchForWifiConnectivityChanges() {
|
||||||
wifiService.networkStatus.collect {
|
wifiService.networkStatus.collect {
|
||||||
when (it) {
|
when (it) {
|
||||||
|
@ -189,20 +218,23 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
|
|
||||||
private suspend fun manageVpn() {
|
private suspend fun manageVpn() {
|
||||||
while(true) {
|
while(true) {
|
||||||
if(setting.isTunnelOnMobileDataEnabled &&
|
if(isEthernetConnected && setting.isTunnelOnEthernetEnabled && vpnService.getState() == Tunnel.State.DOWN) {
|
||||||
|
ServiceManager.startVpnService(this, tunnelConfig)
|
||||||
|
}
|
||||||
|
if(!isEthernetConnected && setting.isTunnelOnMobileDataEnabled &&
|
||||||
!isWifiConnected &&
|
!isWifiConnected &&
|
||||||
isMobileDataConnected
|
isMobileDataConnected
|
||||||
&& vpnService.getState() == Tunnel.State.DOWN) {
|
&& vpnService.getState() == Tunnel.State.DOWN) {
|
||||||
ServiceManager.startVpnService(this, tunnelConfig)
|
ServiceManager.startVpnService(this, tunnelConfig)
|
||||||
} else if(!setting.isTunnelOnMobileDataEnabled &&
|
} else if(!isEthernetConnected && !setting.isTunnelOnMobileDataEnabled &&
|
||||||
!isWifiConnected &&
|
!isWifiConnected &&
|
||||||
vpnService.getState() == Tunnel.State.UP) {
|
vpnService.getState() == Tunnel.State.UP) {
|
||||||
ServiceManager.stopVpnService(this)
|
ServiceManager.stopVpnService(this)
|
||||||
} else if(isWifiConnected &&
|
} else if(!isEthernetConnected && isWifiConnected &&
|
||||||
!setting.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
|
!setting.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
|
||||||
(vpnService.getState() != Tunnel.State.UP)) {
|
(vpnService.getState() != Tunnel.State.UP)) {
|
||||||
ServiceManager.startVpnService(this, tunnelConfig)
|
ServiceManager.startVpnService(this, tunnelConfig)
|
||||||
} else if((isWifiConnected &&
|
} else if(!isEthernetConnected && (isWifiConnected &&
|
||||||
setting.trustedNetworkSSIDs.contains(currentNetworkSSID)) &&
|
setting.trustedNetworkSSIDs.contains(currentNetworkSSID)) &&
|
||||||
(vpnService.getState() == Tunnel.State.UP)) {
|
(vpnService.getState() == Tunnel.State.UP)) {
|
||||||
ServiceManager.stopVpnService(this)
|
ServiceManager.stopVpnService(this)
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.service.network
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class EthernetService @Inject constructor(@ApplicationContext context: Context) :
|
||||||
|
BaseNetworkService<EthernetService>(context, NetworkCapabilities.TRANSPORT_ETHERNET) {
|
||||||
|
}
|
|
@ -46,9 +46,11 @@ object ShortcutsManager {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeTunnelShortcuts(context : Context, tunnelConfig : TunnelConfig) {
|
fun removeTunnelShortcuts(context : Context, tunnelConfig : TunnelConfig?) {
|
||||||
ShortcutManagerCompat.removeDynamicShortcuts(context, listOf(tunnelConfig.id.toString() + APPEND_ON,
|
if(tunnelConfig != null) {
|
||||||
tunnelConfig.id.toString() + APPEND_OFF ))
|
ShortcutManagerCompat.removeDynamicShortcuts(context, listOf(tunnelConfig.id.toString() + APPEND_ON,
|
||||||
|
tunnelConfig.id.toString() + APPEND_OFF ))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createTunnelOnIntent(context: Context, extras : Map<String,String>) : Intent {
|
private fun createTunnelOnIntent(context: Context, extras : Map<String,String>) : Intent {
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.ui;
|
|
||||||
|
|
||||||
import com.journeyapps.barcodescanner.CaptureActivity;
|
|
||||||
|
|
||||||
public class CaptureActivityPortrait extends CaptureActivity {
|
|
||||||
}
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.ui
|
||||||
|
|
||||||
|
import com.journeyapps.barcodescanner.CaptureActivity
|
||||||
|
|
||||||
|
class CaptureActivityPortrait : CaptureActivity()
|
|
@ -44,6 +44,7 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.TransparentSystemBars
|
import com.zaneschepke.wireguardautotunnel.ui.theme.TransparentSystemBars
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.lang.IllegalStateException
|
import java.lang.IllegalStateException
|
||||||
|
@ -101,7 +102,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
} else -> {
|
} else -> {
|
||||||
false;
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -131,8 +132,8 @@ class MainActivity : AppCompatActivity() {
|
||||||
val intentSettings =
|
val intentSettings =
|
||||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||||
intentSettings.data =
|
intentSettings.data =
|
||||||
Uri.fromParts("package", this.packageName, null)
|
Uri.fromParts(Constants.URI_PACKAGE_SCHEME, this.packageName, null)
|
||||||
startActivity(intentSettings);
|
startActivity(intentSettings)
|
||||||
},
|
},
|
||||||
message = getString(R.string.notification_permission_required),
|
message = getString(R.string.notification_permission_required),
|
||||||
getString(R.string.open_settings)
|
getString(R.string.open_settings)
|
||||||
|
@ -190,10 +191,19 @@ class MainActivity : AppCompatActivity() {
|
||||||
}) { SupportScreen(padding = padding, focusRequester) }
|
}) { SupportScreen(padding = padding, focusRequester) }
|
||||||
composable("${Routes.Config.name}/{id}", enterTransition = {
|
composable("${Routes.Config.name}/{id}", enterTransition = {
|
||||||
fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION))
|
fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION))
|
||||||
}) { ConfigScreen(padding = padding, navController = navController, id = it.arguments?.getString("id"), focusRequester = focusRequester)}
|
}) {
|
||||||
|
val id = it.arguments?.getString("id")
|
||||||
|
if(!id.isNullOrBlank()) {
|
||||||
|
ConfigScreen(padding = padding, navController = navController, id = id, focusRequester = focusRequester)}
|
||||||
|
}
|
||||||
composable("${Routes.Detail.name}/{id}", enterTransition = {
|
composable("${Routes.Detail.name}/{id}", enterTransition = {
|
||||||
fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION))
|
fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION))
|
||||||
}) { DetailScreen(padding = padding, id = it.arguments?.getString("id")) }
|
}) {
|
||||||
|
val id = it.arguments?.getString("id")
|
||||||
|
if(!id.isNullOrBlank()) {
|
||||||
|
DetailScreen(padding = padding, id = id)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ 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
|
||||||
|
@ -53,7 +54,7 @@ fun ConfigScreen(
|
||||||
padding: PaddingValues,
|
padding: PaddingValues,
|
||||||
focusRequester: FocusRequester,
|
focusRequester: FocusRequester,
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
id : String?
|
id : String
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
@ -67,11 +68,12 @@ fun ConfigScreen(
|
||||||
val checkedPackages by viewModel.checkedPackages.collectAsStateWithLifecycle()
|
val checkedPackages by viewModel.checkedPackages.collectAsStateWithLifecycle()
|
||||||
val include by viewModel.include.collectAsStateWithLifecycle()
|
val include by viewModel.include.collectAsStateWithLifecycle()
|
||||||
val allApplications by viewModel.allApplications.collectAsStateWithLifecycle()
|
val allApplications by viewModel.allApplications.collectAsStateWithLifecycle()
|
||||||
|
val sortedPackages = remember(packages) {
|
||||||
|
packages.sortedBy { viewModel.getPackageLabel(it) }
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.getTunnelById(id)
|
viewModel.emitScreenData(id)
|
||||||
viewModel.emitQueriedPackages("")
|
|
||||||
viewModel.emitCurrentPackageConfigurations(id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(tunnel != null) {
|
if(tunnel != null) {
|
||||||
|
@ -174,7 +176,7 @@ fun ConfigScreen(
|
||||||
SearchBar(viewModel::emitQueriedPackages);
|
SearchBar(viewModel::emitQueriedPackages);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
items(packages) { pack ->
|
items(sortedPackages, key = { it.packageName }) { pack ->
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
@ -200,8 +202,7 @@ fun ConfigScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
pack.applicationInfo.loadLabel(context.packageManager)
|
viewModel.getPackageLabel(pack), modifier = Modifier.padding(5.dp)
|
||||||
.toString(), modifier = Modifier.padding(5.dp)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Checkbox(
|
Checkbox(
|
||||||
|
|
|
@ -9,11 +9,14 @@ 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 androidx.lifecycle.viewModelScope
|
||||||
|
import com.wireguard.config.Config
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
||||||
import com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutsManager
|
import com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutsManager
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
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 kotlinx.coroutines.launch
|
||||||
|
@ -41,24 +44,37 @@ class ConfigViewModel @Inject constructor(private val application : Application,
|
||||||
private val _allApplications = MutableStateFlow(true)
|
private val _allApplications = MutableStateFlow(true)
|
||||||
val allApplications get() = _allApplications.asStateFlow()
|
val allApplications get() = _allApplications.asStateFlow()
|
||||||
|
|
||||||
suspend fun getTunnelById(id : String?) : TunnelConfig? {
|
fun emitScreenData(id : String) {
|
||||||
return try {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
if(id != null) {
|
val tunnelConfig = getTunnelConfigById(id);
|
||||||
val config = tunnelRepo.getById(id.toLong())
|
emitTunnelConfig(tunnelConfig);
|
||||||
if (config != null) {
|
emitTunnelConfigName(tunnelConfig?.name)
|
||||||
_tunnel.emit(config)
|
emitQueriedPackages("")
|
||||||
_tunnelName.emit(config.name)
|
emitCurrentPackageConfigurations(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
private suspend fun getTunnelConfigById(id : String) : TunnelConfig? {
|
||||||
return config
|
return try {
|
||||||
}
|
tunnelRepo.getById(id.toLong())
|
||||||
return null
|
|
||||||
} catch (e : Exception) {
|
} catch (e : Exception) {
|
||||||
Timber.e(e.message)
|
Timber.e(e.message)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun emitTunnelConfig(tunnelConfig: TunnelConfig?) {
|
||||||
|
if(tunnelConfig != null) {
|
||||||
|
_tunnel.emit(tunnelConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun emitTunnelConfigName(name : String?) {
|
||||||
|
if(name != null) {
|
||||||
|
_tunnelName.emit(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun onTunnelNameChange(name : String) {
|
fun onTunnelNameChange(name : String) {
|
||||||
_tunnelName.value = name
|
_tunnelName.value = name
|
||||||
}
|
}
|
||||||
|
@ -78,35 +94,71 @@ class ConfigViewModel @Inject constructor(private val application : Application,
|
||||||
_checkedPackages.value.remove(packageName)
|
_checkedPackages.value.remove(packageName)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun emitCurrentPackageConfigurations(id : String?) {
|
private suspend fun emitSplitTunnelConfiguration(config : Config) {
|
||||||
val tunnelConfig = getTunnelById(id)
|
val excludedApps = config.`interface`.excludedApplications
|
||||||
if(tunnelConfig != null) {
|
val includedApps = config.`interface`.includedApplications
|
||||||
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
if (excludedApps.isNotEmpty() || includedApps.isNotEmpty()) {
|
||||||
val excludedApps = config.`interface`.excludedApplications
|
emitTunnelAllApplicationsDisabled()
|
||||||
val includedApps = config.`interface`.includedApplications
|
determineAppInclusionState(excludedApps, includedApps)
|
||||||
if(excludedApps.isNullOrEmpty() && includedApps.isNullOrEmpty()) {
|
} else {
|
||||||
_allApplications.emit(true)
|
emitTunnelAllApplicationsEnabled()
|
||||||
return
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun determineAppInclusionState(excludedApps : Set<String>, includedApps : Set<String>) {
|
||||||
|
if (excludedApps.isEmpty()) {
|
||||||
|
emitIncludedAppsExist()
|
||||||
|
emitCheckedApps(includedApps)
|
||||||
|
} else {
|
||||||
|
emitExcludedAppsExist()
|
||||||
|
emitCheckedApps(excludedApps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun emitIncludedAppsExist() {
|
||||||
|
_include.emit(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun emitExcludedAppsExist() {
|
||||||
|
_include.emit(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun emitCheckedApps(apps : Set<String>) {
|
||||||
|
_checkedPackages.emit(apps.toMutableStateList())
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun emitTunnelAllApplicationsEnabled() {
|
||||||
|
_allApplications.emit(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun emitTunnelAllApplicationsDisabled() {
|
||||||
|
_allApplications.emit(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun emitCurrentPackageConfigurations(id : String) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val tunnelConfig = getTunnelConfigById(id)
|
||||||
|
if (tunnelConfig != null) {
|
||||||
|
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
||||||
|
emitSplitTunnelConfiguration(config)
|
||||||
}
|
}
|
||||||
if(excludedApps.isEmpty()) {
|
|
||||||
_include.emit(true)
|
|
||||||
_checkedPackages.emit(includedApps.toMutableStateList())
|
|
||||||
} else {
|
|
||||||
_include.emit(false)
|
|
||||||
_checkedPackages.emit(excludedApps.toMutableStateList())
|
|
||||||
}
|
|
||||||
_allApplications.emit(false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun emitQueriedPackages(query : String) {
|
fun emitQueriedPackages(query : String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
_packages.emit(getAllInternetCapablePackages().filter {
|
val packages = getAllInternetCapablePackages().filter {
|
||||||
it.packageName.contains(query)
|
getPackageLabel(it).lowercase().contains(query.lowercase())
|
||||||
})
|
}
|
||||||
|
_packages.emit(packages)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getPackageLabel(packageInfo : PackageInfo) : String {
|
||||||
|
return packageInfo.applicationInfo.loadLabel(application.packageManager).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun getAllInternetCapablePackages() : List<PackageInfo> {
|
private fun getAllInternetCapablePackages() : List<PackageInfo> {
|
||||||
return getPackagesHoldingPermissions(arrayOf(Manifest.permission.INTERNET))
|
return getPackagesHoldingPermissions(arrayOf(Manifest.permission.INTERNET))
|
||||||
}
|
}
|
||||||
|
@ -119,39 +171,77 @@ class ConfigViewModel @Inject constructor(private val application : Application,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun onSaveAllChanges() {
|
private fun removeTunnelShortcuts(tunnelConfig: TunnelConfig?) {
|
||||||
if(_tunnel.value != null) {
|
if(tunnelConfig != null) {
|
||||||
ShortcutsManager.removeTunnelShortcuts(application, _tunnel.value!!)
|
ShortcutsManager.removeTunnelShortcuts(application, tunnelConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isAllApplicationsEnabled() : Boolean {
|
||||||
|
return _allApplications.value
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isIncludeApplicationsEnabled() : Boolean {
|
||||||
|
return _include.value
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateQuickStringWithSelectedPackages() : String {
|
||||||
var wgQuick = _tunnel.value?.wgQuick
|
var wgQuick = _tunnel.value?.wgQuick
|
||||||
if(wgQuick != null) {
|
if(wgQuick != null) {
|
||||||
wgQuick = if(_include.value) {
|
wgQuick = if(isAllApplicationsEnabled()) {
|
||||||
|
TunnelConfig.clearAllApplicationsFromConfig(wgQuick)
|
||||||
|
} else if(isIncludeApplicationsEnabled()) {
|
||||||
TunnelConfig.setIncludedApplicationsOnQuick(_checkedPackages.value, wgQuick)
|
TunnelConfig.setIncludedApplicationsOnQuick(_checkedPackages.value, wgQuick)
|
||||||
} else {
|
} else {
|
||||||
TunnelConfig.setExcludedApplicationsOnQuick(_checkedPackages.value, wgQuick)
|
TunnelConfig.setExcludedApplicationsOnQuick(_checkedPackages.value, wgQuick)
|
||||||
}
|
}
|
||||||
if(_allApplications.value) {
|
} else {
|
||||||
wgQuick = TunnelConfig.clearAllApplicationsFromConfig(wgQuick)
|
throw WgTunnelException("Wg quick string is null")
|
||||||
}
|
}
|
||||||
_tunnel.value?.copy(
|
return wgQuick;
|
||||||
name = _tunnelName.value,
|
}
|
||||||
wgQuick = wgQuick
|
|
||||||
)?.let {
|
private suspend fun saveConfig(tunnelConfig: TunnelConfig) {
|
||||||
tunnelRepo.save(it)
|
tunnelRepo.save(tunnelConfig)
|
||||||
ShortcutsManager.createTunnelShortcuts(application, it)
|
}
|
||||||
val settings = settingsRepo.getAll()
|
private suspend fun updateTunnelConfig(tunnelConfig: TunnelConfig?) {
|
||||||
if(settings.isEmpty()) {
|
if(tunnelConfig != null) {
|
||||||
return
|
saveConfig(tunnelConfig)
|
||||||
}
|
addTunnelShortcuts(tunnelConfig)
|
||||||
val setting = settings[0]
|
updateSettingsDefaultTunnel(tunnelConfig)
|
||||||
if(setting.defaultTunnel != null) {
|
}
|
||||||
if(it.id == TunnelConfig.from(setting.defaultTunnel!!).id) {
|
}
|
||||||
settingsRepo.save(setting.copy(
|
|
||||||
defaultTunnel = it.toString()
|
private suspend fun updateSettingsDefaultTunnel(tunnelConfig: TunnelConfig) {
|
||||||
))
|
val settings = settingsRepo.getAll()
|
||||||
}
|
if(settings.isNotEmpty()) {
|
||||||
|
val setting = settings[0]
|
||||||
|
if(setting.defaultTunnel != null) {
|
||||||
|
if(tunnelConfig.id == TunnelConfig.from(setting.defaultTunnel!!).id) {
|
||||||
|
settingsRepo.save(setting.copy(
|
||||||
|
defaultTunnel = tunnelConfig.toString()
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun addTunnelShortcuts(tunnelConfig: TunnelConfig) {
|
||||||
|
ShortcutsManager.createTunnelShortcuts(application, tunnelConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun onSaveAllChanges() {
|
||||||
|
try {
|
||||||
|
removeTunnelShortcuts(_tunnel.value)
|
||||||
|
val wgQuick = updateQuickStringWithSelectedPackages()
|
||||||
|
val tunnelConfig = _tunnel.value?.copy(
|
||||||
|
name = _tunnelName.value,
|
||||||
|
wgQuick = wgQuick
|
||||||
|
)
|
||||||
|
updateTunnelConfig(tunnelConfig)
|
||||||
|
} catch (e : Exception) {
|
||||||
|
Timber.e(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -36,7 +36,7 @@ import java.time.Instant
|
||||||
fun DetailScreen(
|
fun DetailScreen(
|
||||||
viewModel: DetailViewModel = hiltViewModel(),
|
viewModel: DetailViewModel = hiltViewModel(),
|
||||||
padding: PaddingValues,
|
padding: PaddingValues,
|
||||||
id : String?
|
id : String
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
||||||
|
@ -47,15 +47,17 @@ fun DetailScreen(
|
||||||
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.getTunnelById(id)
|
viewModel.emitConfig(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
if(tunnel != null) {
|
if(null != tunnel) {
|
||||||
val interfaceKey = tunnel?.`interface`?.keyPair?.publicKey?.toBase64().toString()
|
val interfaceKey = tunnel?.`interface`?.keyPair?.publicKey?.toBase64().toString()
|
||||||
val addresses = tunnel?.`interface`?.addresses!!.joinToString()
|
val addresses = tunnel?.`interface`?.addresses!!.joinToString()
|
||||||
val dnsServers = tunnel?.`interface`?.dnsServers!!.joinToString()
|
val dnsServers = tunnel?.`interface`?.dnsServers!!.joinToString()
|
||||||
val optionalMtu = tunnel?.`interface`?.mtu
|
val optionalMtu = tunnel?.`interface`?.mtu
|
||||||
val mtu = if(optionalMtu?.isPresent == true) optionalMtu.get().toString() else "None"
|
val mtu = if(optionalMtu?.isPresent == true) optionalMtu.get().toString() else stringResource(
|
||||||
|
id = R.string.none
|
||||||
|
)
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
|
@ -97,7 +99,9 @@ fun DetailScreen(
|
||||||
tunnel?.peers?.forEach{
|
tunnel?.peers?.forEach{
|
||||||
val peerKey = it.publicKey.toBase64().toString()
|
val peerKey = it.publicKey.toBase64().toString()
|
||||||
val allowedIps = it.allowedIps.joinToString()
|
val allowedIps = it.allowedIps.joinToString()
|
||||||
val endpoint = if(it.endpoint.isPresent) it.endpoint.get().toString() else "None"
|
val endpoint = if(it.endpoint.isPresent) it.endpoint.get().toString() else stringResource(
|
||||||
|
id = R.string.none
|
||||||
|
)
|
||||||
Text(stringResource(R.string.peer), fontWeight = FontWeight.Bold, fontSize = 20.sp)
|
Text(stringResource(R.string.peer), fontWeight = FontWeight.Bold, fontSize = 20.sp)
|
||||||
Text(stringResource(R.string.public_key), fontStyle = FontStyle.Italic)
|
Text(stringResource(R.string.public_key), fontStyle = FontStyle.Italic)
|
||||||
Text(text = peerKey, modifier = Modifier.clickable {
|
Text(text = peerKey, modifier = Modifier.clickable {
|
||||||
|
@ -123,7 +127,7 @@ fun DetailScreen(
|
||||||
val handshakeEpoch = lastHandshake[it.publicKey]
|
val handshakeEpoch = lastHandshake[it.publicKey]
|
||||||
if(handshakeEpoch != null) {
|
if(handshakeEpoch != null) {
|
||||||
if(handshakeEpoch == 0L) {
|
if(handshakeEpoch == 0L) {
|
||||||
Text("Never")
|
Text(stringResource(id = R.string.never))
|
||||||
} else {
|
} else {
|
||||||
val time = Instant.ofEpochMilli(handshakeEpoch)
|
val time = Instant.ofEpochMilli(handshakeEpoch)
|
||||||
Text("${Duration.between(time, Instant.now()).seconds} seconds ago")
|
Text("${Duration.between(time, Instant.now()).seconds} seconds ago")
|
||||||
|
|
|
@ -1,45 +1,45 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.detail
|
package com.zaneschepke.wireguardautotunnel.ui.screens.detail
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.wireguard.config.Config
|
import com.wireguard.config.Config
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
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
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class DetailViewModel @Inject constructor(private val tunnelRepo : TunnelConfigDao, private val vpnService : VpnService
|
class DetailViewModel @Inject constructor(private val tunnelRepo : TunnelConfigDao, private val vpnService : VpnService
|
||||||
|
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _tunnel = MutableStateFlow<Config?>(null)
|
private val _tunnel = MutableStateFlow<Config?>(null)
|
||||||
val tunnel get() = _tunnel.asStateFlow()
|
val tunnel get() = _tunnel.asStateFlow()
|
||||||
|
|
||||||
private val _tunnelName = MutableStateFlow<String>("")
|
private val _tunnelName = MutableStateFlow("")
|
||||||
val tunnelName = _tunnelName.asStateFlow()
|
val tunnelName = _tunnelName.asStateFlow()
|
||||||
val tunnelStats get() = vpnService.statistics
|
val tunnelStats get() = vpnService.statistics
|
||||||
val lastHandshake get() = vpnService.lastHandshake
|
val lastHandshake get() = vpnService.lastHandshake
|
||||||
|
|
||||||
private var config : TunnelConfig? = null
|
private suspend fun getTunnelConfigById(id: String): TunnelConfig? {
|
||||||
|
|
||||||
suspend fun getTunnelById(id : String?) : TunnelConfig? {
|
|
||||||
return try {
|
return try {
|
||||||
if(id != null) {
|
tunnelRepo.getById(id.toLong())
|
||||||
config = tunnelRepo.getById(id.toLong())
|
} catch (e: Exception) {
|
||||||
if (config != null) {
|
|
||||||
_tunnel.emit(TunnelConfig.configFromQuick(config!!.wgQuick))
|
|
||||||
_tunnelName.emit(config!!.name)
|
|
||||||
}
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
} catch (e : Exception) {
|
|
||||||
Timber.e(e.message)
|
Timber.e(e.message)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fun emitConfig(id: String) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val tunnelConfig = getTunnelConfigById(id)
|
||||||
|
if(tunnelConfig != null) {
|
||||||
|
_tunnel.emit(TunnelConfig.configFromQuick(tunnelConfig.wgQuick))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -73,11 +73,12 @@ import androidx.navigation.NavController
|
||||||
import com.journeyapps.barcodescanner.ScanContract
|
import com.journeyapps.barcodescanner.ScanContract
|
||||||
import com.journeyapps.barcodescanner.ScanOptions
|
import com.journeyapps.barcodescanner.ScanOptions
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait
|
import com.zaneschepke.wireguardautotunnel.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.Routes
|
import com.zaneschepke.wireguardautotunnel.ui.Routes
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
|
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.brickRed
|
import com.zaneschepke.wireguardautotunnel.ui.theme.brickRed
|
||||||
|
@ -146,7 +147,13 @@ fun MainScreen(
|
||||||
|
|
||||||
val scanLauncher = rememberLauncherForActivityResult(
|
val scanLauncher = rememberLauncherForActivityResult(
|
||||||
contract = ScanContract(),
|
contract = ScanContract(),
|
||||||
onResult = { result -> viewModel.onTunnelQrResult(result.contents) }
|
onResult = {
|
||||||
|
try {
|
||||||
|
viewModel.onTunnelQrResult(it.contents)
|
||||||
|
} catch (e : Exception) {
|
||||||
|
viewModel.showSnackBarMessage(context.getString(R.string.qr_result_failed))
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
@ -205,7 +212,7 @@ fun MainScreen(
|
||||||
showBottomSheet = false
|
showBottomSheet = false
|
||||||
val fileSelectionIntent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
val fileSelectionIntent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
type = "*/*"
|
type = Constants.ALLOWED_FILE_TYPES
|
||||||
}
|
}
|
||||||
pickFileLauncher.launch(fileSelectionIntent)
|
pickFileLauncher.launch(fileSelectionIntent)
|
||||||
}
|
}
|
||||||
|
@ -232,7 +239,7 @@ fun MainScreen(
|
||||||
scanOptions.setOrientationLocked(true)
|
scanOptions.setOrientationLocked(true)
|
||||||
scanOptions.setPrompt(context.getString(R.string.scanning_qr))
|
scanOptions.setPrompt(context.getString(R.string.scanning_qr))
|
||||||
scanOptions.setBeepEnabled(false)
|
scanOptions.setBeepEnabled(false)
|
||||||
scanOptions.captureActivity = CaptureActivityPortrait().javaClass
|
scanOptions.captureActivity = CaptureActivityPortrait::class.java
|
||||||
scanLauncher.launch(scanOptions)
|
scanLauncher.launch(scanOptions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -264,7 +271,7 @@ fun MainScreen(
|
||||||
.nestedScroll(nestedScrollConnection),
|
.nestedScroll(nestedScrollConnection),
|
||||||
) {
|
) {
|
||||||
items(tunnels, key = { tunnel -> tunnel.id }) {tunnel ->
|
items(tunnels, key = { tunnel -> tunnel.id }) {tunnel ->
|
||||||
val focusRequester = FocusRequester();
|
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
|
||||||
|
@ -281,7 +288,7 @@ fun MainScreen(
|
||||||
return@RowListItem
|
return@RowListItem
|
||||||
}
|
}
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
selectedTunnel = tunnel;
|
selectedTunnel = tunnel
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
if (!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.database.Cursor
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
@ -18,18 +18,21 @@ import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceState
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceState
|
||||||
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.shortcut.ShortcutsManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.ViewState
|
import com.zaneschepke.wireguardautotunnel.ui.ViewState
|
||||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import java.io.InputStream
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
|
@ -86,88 +89,164 @@ class MainViewModel @Inject constructor(private val application : Application,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onTunnelStart(tunnelConfig : TunnelConfig) = viewModelScope.launch {
|
fun onTunnelStart(tunnelConfig : TunnelConfig) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
stopActiveTunnel()
|
||||||
|
startTunnel(tunnelConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startTunnel(tunnelConfig: TunnelConfig) {
|
||||||
ServiceManager.startVpnService(application.applicationContext, tunnelConfig.toString())
|
ServiceManager.startVpnService(application.applicationContext, tunnelConfig.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun stopActiveTunnel() {
|
||||||
|
if(ServiceManager.getServiceState(application.applicationContext,
|
||||||
|
WireGuardTunnelService::class.java, ) == ServiceState.STARTED) {
|
||||||
|
onTunnelStop()
|
||||||
|
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun onTunnelStop() {
|
fun onTunnelStop() {
|
||||||
ServiceManager.stopVpnService(application.applicationContext)
|
ServiceManager.stopVpnService(application.applicationContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun validateConfigString(config : String) {
|
||||||
|
if(!config.contains(application.getString(R.string.config_validation))) {
|
||||||
|
throw WgTunnelException(application.getString(R.string.config_validation))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun onTunnelQrResult(result : String) {
|
fun onTunnelQrResult(result : String) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
if(result.contains(application.resources.getString(R.string.config_validation))) {
|
try {
|
||||||
|
validateConfigString(result)
|
||||||
val tunnelConfig = TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result)
|
val tunnelConfig = TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result)
|
||||||
saveTunnel(tunnelConfig)
|
addTunnel(tunnelConfig)
|
||||||
} else {
|
} catch (e : WgTunnelException) {
|
||||||
showSnackBarMessage(application.resources.getString(R.string.barcode_error))
|
showSnackBarMessage(e.message ?: application.getString(R.string.unknown_error_message))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onTunnelFileSelected(uri : Uri) {
|
private fun validateFileExtension(fileName : String) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
val extension = getFileExtensionFromFileName(fileName)
|
||||||
try {
|
if(extension != Constants.VALID_FILE_EXTENSION) {
|
||||||
val fileName = getFileName(application.applicationContext, uri)
|
throw WgTunnelException(application.getString(R.string.file_extension_message))
|
||||||
val extension = getFileExtensionFromFileName(fileName)
|
|
||||||
if (extension != ".conf") {
|
|
||||||
launch {
|
|
||||||
showSnackBarMessage(application.resources.getString(R.string.file_extension_message))
|
|
||||||
}
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
val stream = application.applicationContext.contentResolver.openInputStream(uri)
|
|
||||||
stream ?: return@launch
|
|
||||||
val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8)
|
|
||||||
val config = Config.parse(bufferReader)
|
|
||||||
val tunnelName = getNameFromFileName(fileName)
|
|
||||||
saveTunnel(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString()))
|
|
||||||
stream.close()
|
|
||||||
} catch (_: BadConfigException) {
|
|
||||||
launch {
|
|
||||||
showSnackBarMessage(application.applicationContext.getString(R.string.bad_config))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun saveTunnelConfigFromStream(stream : InputStream, fileName : String) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8)
|
||||||
|
val config = Config.parse(bufferReader)
|
||||||
|
val tunnelName = getNameFromFileName(fileName)
|
||||||
|
addTunnel(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString()))
|
||||||
|
stream.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getInputStreamFromUri(uri: Uri): InputStream {
|
||||||
|
return application.applicationContext.contentResolver.openInputStream(uri)
|
||||||
|
?: throw WgTunnelException(application.getString(R.string.stream_failed))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onTunnelFileSelected(uri : Uri) {
|
||||||
|
try {
|
||||||
|
val fileName = getFileName(application.applicationContext, uri)
|
||||||
|
validateFileExtension(fileName)
|
||||||
|
val stream = getInputStreamFromUri(uri)
|
||||||
|
saveTunnelConfigFromStream(stream, fileName)
|
||||||
|
} catch (e : Exception) {
|
||||||
|
showExceptionMessage(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showExceptionMessage(e : Exception) {
|
||||||
|
when(e) {
|
||||||
|
is BadConfigException -> {
|
||||||
|
showSnackBarMessage(application.getString(R.string.bad_config))
|
||||||
|
}
|
||||||
|
is WgTunnelException -> {
|
||||||
|
showSnackBarMessage(e.message ?: application.getString(R.string.unknown_error_message))
|
||||||
|
}
|
||||||
|
else -> showSnackBarMessage(application.getString(R.string.unknown_error_message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
|
||||||
|
saveTunnel(tunnelConfig)
|
||||||
|
createTunnelAppShortcuts(tunnelConfig)
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun saveTunnel(tunnelConfig : TunnelConfig) {
|
private suspend fun saveTunnel(tunnelConfig : TunnelConfig) {
|
||||||
tunnelRepo.save(tunnelConfig)
|
tunnelRepo.save(tunnelConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createTunnelAppShortcuts(tunnelConfig: TunnelConfig) {
|
||||||
ShortcutsManager.createTunnelShortcuts(application.applicationContext, tunnelConfig)
|
ShortcutsManager.createTunnelShortcuts(application.applicationContext, tunnelConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("Range")
|
private fun getFileNameByCursor(context: Context, uri: Uri) : String {
|
||||||
private fun getFileName(context: Context, uri: Uri): String {
|
val cursor = context.contentResolver.query(uri, null, null, null, null)
|
||||||
if (uri.scheme == "content") {
|
if(cursor != null) {
|
||||||
val cursor = try {
|
|
||||||
context.contentResolver.query(uri, null, null, null, null)
|
|
||||||
} catch (e : Exception) {
|
|
||||||
Timber.d("Exception getting config name")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
cursor ?: return NumberUtils.generateRandomTunnelName()
|
|
||||||
cursor.use {
|
cursor.use {
|
||||||
if(cursor.moveToFirst()) {
|
return getDisplayNameByCursor(it)
|
||||||
return cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
throw WgTunnelException("Failed to initialize cursor")
|
||||||
}
|
}
|
||||||
return NumberUtils.generateRandomTunnelName()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun showSnackBarMessage(message : String) {
|
private fun getDisplayNameColumnIndex(cursor: Cursor) : Int {
|
||||||
_viewState.emit(_viewState.value.copy(
|
val columnIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||||
showSnackbarMessage = true,
|
if(columnIndex == -1) {
|
||||||
snackbarMessage = message,
|
throw WgTunnelException("Cursor out of bounds")
|
||||||
snackbarActionText = "Okay",
|
}
|
||||||
onSnackbarActionClick = {
|
return columnIndex
|
||||||
viewModelScope.launch {
|
}
|
||||||
dismissSnackBar()
|
|
||||||
|
private fun getDisplayNameByCursor(cursor: Cursor) : String {
|
||||||
|
if(cursor.moveToFirst()) {
|
||||||
|
val index = getDisplayNameColumnIndex(cursor)
|
||||||
|
return cursor.getString(index)
|
||||||
|
} else {
|
||||||
|
throw WgTunnelException("Cursor failed to move to first")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validateUriContentScheme(uri : Uri) {
|
||||||
|
if (uri.scheme != Constants.URI_CONTENT_SCHEME) {
|
||||||
|
throw WgTunnelException(application.getString(R.string.file_extension_message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun getFileName(context: Context, uri: Uri): String {
|
||||||
|
validateUriContentScheme(uri)
|
||||||
|
return try {
|
||||||
|
getFileNameByCursor(context, uri)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
NumberUtils.generateRandomTunnelName()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showSnackBarMessage(message : String) {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
_viewState.emit(_viewState.value.copy(
|
||||||
|
showSnackbarMessage = true,
|
||||||
|
snackbarMessage = message,
|
||||||
|
snackbarActionText = application.getString(R.string.okay),
|
||||||
|
onSnackbarActionClick = {
|
||||||
|
viewModelScope.launch {
|
||||||
|
dismissSnackBar()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
))
|
||||||
))
|
delay(Constants.SNACKBAR_DELAY)
|
||||||
delay(Constants.SNACKBAR_DELAY)
|
dismissSnackBar()
|
||||||
dismissSnackBar()
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun dismissSnackBar() {
|
private suspend fun dismissSnackBar() {
|
||||||
|
|
|
@ -392,6 +392,24 @@ fun SettingsScreen(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(screenPadding),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text("Tunnel on Ethernet")
|
||||||
|
Switch(
|
||||||
|
enabled = !settings.isAutoTunnelEnabled,
|
||||||
|
checked = settings.isTunnelOnEthernetEnabled,
|
||||||
|
onCheckedChange = {
|
||||||
|
scope.launch {
|
||||||
|
viewModel.onToggleTunnelOnEthernet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|
|
@ -122,6 +122,18 @@ class SettingsViewModel @Inject constructor(private val application : Applicatio
|
||||||
showSnackBarMessage(application.getString(R.string.select_tunnel_message))
|
showSnackBarMessage(application.getString(R.string.select_tunnel_message))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun onToggleTunnelOnEthernet() {
|
||||||
|
if(_settings.value.defaultTunnel != null) {
|
||||||
|
_settings.emit(
|
||||||
|
_settings.value.copy(isTunnelOnEthernetEnabled = !_settings.value.isTunnelOnEthernetEnabled)
|
||||||
|
)
|
||||||
|
settingsRepo.save(_settings.value)
|
||||||
|
} else {
|
||||||
|
showSnackBarMessage(application.getString(R.string.select_tunnel_message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun checkLocationServicesEnabled() : Boolean {
|
fun checkLocationServicesEnabled() : Boolean {
|
||||||
val locationManager =
|
val locationManager =
|
||||||
application.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
application.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.util
|
||||||
|
|
||||||
|
class WgTunnelException(message: String) : Exception(message)
|
|
@ -38,7 +38,6 @@
|
||||||
<string name="trusted_ssid_empty_description">Enter SSID</string>
|
<string name="trusted_ssid_empty_description">Enter SSID</string>
|
||||||
<string name="trusted_ssid_value_description">Submit SSID</string>
|
<string name="trusted_ssid_value_description">Submit SSID</string>
|
||||||
<string name="config_validation">[Interface]</string>
|
<string name="config_validation">[Interface]</string>
|
||||||
<string name="invalid_qr">Invalid QR code.</string>
|
|
||||||
<string name="add_from_file">Add tunnel from files</string>
|
<string name="add_from_file">Add tunnel from files</string>
|
||||||
<string name="open_file">File Open</string>
|
<string name="open_file">File Open</string>
|
||||||
<string name="add_from_qr">Add tunnel from QR code</string>
|
<string name="add_from_qr">Add tunnel from QR code</string>
|
||||||
|
@ -62,7 +61,6 @@
|
||||||
<string name="public_key">Public key</string>
|
<string name="public_key">Public key</string>
|
||||||
<string name="barcode_downloading">Waiting for the Barcode UI module to be downloaded.</string>
|
<string name="barcode_downloading">Waiting for the Barcode UI module to be downloaded.</string>
|
||||||
<string name="barcode_downloading_message">Barcode module downloading. Try again.</string>
|
<string name="barcode_downloading_message">Barcode module downloading. Try again.</string>
|
||||||
<string name="barcode_error">Invalid QR code. Try again.</string>
|
|
||||||
<string name="addresses">Addresses</string>
|
<string name="addresses">Addresses</string>
|
||||||
<string name="dns_servers">DNS servers</string>
|
<string name="dns_servers">DNS servers</string>
|
||||||
<string name="mtu">MTU</string>
|
<string name="mtu">MTU</string>
|
||||||
|
@ -92,5 +90,10 @@
|
||||||
<string name="attempt_connection">Attempting connection..</string>
|
<string name="attempt_connection">Attempting connection..</string>
|
||||||
<string name="vpn_starting">VPN Starting</string>
|
<string name="vpn_starting">VPN Starting</string>
|
||||||
<string name="db_name">wg-tunnel-db</string>
|
<string name="db_name">wg-tunnel-db</string>
|
||||||
<string name="scanning_qr">Reading QR code</string>
|
<string name="scanning_qr">Scanning for QR</string>
|
||||||
|
<string name="qr_result_failed">QR scan failed</string>
|
||||||
|
<string name="none">None</string>
|
||||||
|
<string name="never">Never</string>
|
||||||
|
<string name="stream_failed">Failed to open file stream.</string>
|
||||||
|
<string name="unknown_error_message">An unknown error occurred.</string>
|
||||||
</resources>
|
</resources>
|
Loading…
Reference in New Issue