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
This commit is contained in:
Zane Schepke 2023-09-04 02:42:49 -04:00
parent eeccc71469
commit 20cc2c09b0
18 changed files with 346 additions and 201 deletions

View File

@ -16,8 +16,8 @@ android {
compileSdk = 34
val versionMajor = 2
val versionMinor = 3
val versionPatch = 7
val versionMinor = 4
val versionPatch = 1
val versionBuild = 0
defaultConfig {

View File

@ -58,6 +58,10 @@
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
<activity
android:finishOnTaskLaunch="true"
android:theme="@android:style/Theme.NoDisplay"
android:name=".service.shortcut.ShortcutsActivity"/>
<service
android:name=".service.foreground.ForegroundService"
android:enabled="true"
@ -66,7 +70,7 @@
</service>
<service
android:exported="true"
android:name=".service.TunnelControlTile"
android:name=".service.tile.TunnelControlTile"
android:icon="@drawable/shield"
android:label="WG Tunnel"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
@ -82,6 +86,7 @@
android:name=".service.foreground.WireGuardTunnelService"
android:permission="android.permission.BIND_VPN_SERVICE"
android:enabled="true"
android:persistent="true"
android:foregroundServiceType="remoteMessaging"
android:exported="false">
<intent-filter>
@ -94,6 +99,7 @@
android:name=".service.foreground.WireGuardConnectivityWatcherService"
android:enabled="true"
android:stopWithTask="false"
android:persistent="true"
android:foregroundServiceType="location"
android:permission=""
android:exported="false">

View File

@ -3,11 +3,8 @@ package com.zaneschepke.wireguardautotunnel.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceTracker
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
@ -30,12 +27,7 @@ class BootReceiver : BroadcastReceiver() {
if (!settings.isNullOrEmpty()) {
val setting = settings.first()
if (setting.isAutoTunnelEnabled && setting.defaultTunnel != null) {
ServiceTracker.actionOnService(
Action.START, context,
WireGuardConnectivityWatcherService::class.java,
mapOf(context.resources.getString(R.string.tunnel_extras_key) to
setting.defaultTunnel!!)
)
ServiceManager.startWatcherService(context, setting.defaultTunnel!!)
}
}
} finally {

View File

@ -3,11 +3,8 @@ package com.zaneschepke.wireguardautotunnel.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceTracker
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
@ -29,23 +26,9 @@ class NotificationActionReceiver : BroadcastReceiver() {
if (!settings.isNullOrEmpty()) {
val setting = settings.first()
if (setting.defaultTunnel != null) {
ServiceTracker.actionOnService(
Action.STOP, context,
WireGuardTunnelService::class.java,
mapOf(
context.resources.getString(R.string.tunnel_extras_key) to
setting.defaultTunnel!!
)
)
ServiceManager.stopVpnService(context)
delay(1000)
ServiceTracker.actionOnService(
Action.START, context,
WireGuardTunnelService::class.java,
mapOf(
context.resources.getString(R.string.tunnel_extras_key) to
setting.defaultTunnel!!
)
)
ServiceManager.startVpnService(context, setting.defaultTunnel.toString())
}
}
} finally {

View File

@ -0,0 +1,82 @@
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
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 -> context.startForegroundService(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 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)
}
}
}

View File

@ -1,58 +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
if (getServiceState(application, cls) == ServiceState.STARTED && action == Action.START) 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
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 {
context.startService(intent)
} catch (e : Exception) {
e.message?.let { Firebase.crashlytics.log(it) }
}
}
}

View File

@ -1,7 +1,6 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.AlarmManager
import android.app.Application
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
@ -54,7 +53,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
private lateinit var watcherJob : Job;
private lateinit var setting : Settings
private lateinit var tunnelId: String
private lateinit var tunnelConfig: String
private var wakeLock: PowerManager.WakeLock? = null
private val tag = this.javaClass.name;
@ -64,13 +63,13 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
super.startService(extras)
val tunnelId = extras?.getString(getString(R.string.tunnel_extras_key))
if (tunnelId != null) {
this.tunnelId = tunnelId
this.tunnelConfig = tunnelId
}
// we need this lock so our service gets not affected by Doze Mode
initWakeLock()
cancelWatcherJob()
launchWatcherNotification()
if(this::tunnelId.isInitialized) {
if(this::tunnelConfig.isInitialized) {
startWatcherJob()
} else {
stopService(extras)
@ -187,36 +186,21 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
!isWifiConnected &&
isMobileDataConnected
&& vpnService.getState() == Tunnel.State.DOWN) {
startVPN()
ServiceManager.startVpnService(this, tunnelConfig)
} else if(!setting.isTunnelOnMobileDataEnabled &&
!isWifiConnected &&
vpnService.getState() == Tunnel.State.UP) {
stopVPN()
ServiceManager.stopVpnService(this)
} else if(isWifiConnected &&
!setting.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
(vpnService.getState() != Tunnel.State.UP)) {
startVPN()
ServiceManager.startVpnService(this, tunnelConfig)
} else if((isWifiConnected &&
setting.trustedNetworkSSIDs.contains(currentNetworkSSID)) &&
(vpnService.getState() == Tunnel.State.UP)) {
stopVPN()
ServiceManager.stopVpnService(this)
}
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
)
}
}

View File

@ -0,0 +1,28 @@
package com.zaneschepke.wireguardautotunnel.service.shortcut
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class ShortcutsActivity : AppCompatActivity() {
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 {
ServiceManager.toggleWatcherService(this, 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()
}
}

View File

@ -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)
}
}
}
}

View File

@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.service
package com.zaneschepke.wireguardautotunnel.service.tile
import android.os.Build
import android.service.quicksettings.Tile
@ -6,11 +6,7 @@ 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.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceState
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceTracker
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
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
@ -68,9 +64,9 @@ class TunnelControlTile : TileService() {
if(tunnel != null) {
attemptWatcherServiceToggle(tunnel.toString())
if(vpnService.getState() == Tunnel.State.UP) {
stopTunnelService();
ServiceManager.stopVpnService(this@TunnelControlTile)
} else {
startTunnelService(tunnel.toString())
ServiceManager.startVpnService(this@TunnelControlTile, tunnel.toString())
}
}
} catch (e : Exception) {
@ -97,34 +93,6 @@ class TunnelControlTile : TileService() {
return tunnelConfig;
}
private fun stopTunnelService() {
ServiceTracker.actionOnService(
Action.STOP, this.applicationContext,
WireGuardTunnelService::class.java)
}
private fun startTunnelService(tunnelConfig : String) {
ServiceTracker.actionOnService(
Action.START, this.applicationContext,
WireGuardTunnelService::class.java,
mapOf(this.applicationContext.resources.
getString(R.string.tunnel_extras_key) to
tunnelConfig))
}
private fun startWatcherService(tunnelConfig : String) {
ServiceTracker.actionOnService(
Action.START, this,
WireGuardConnectivityWatcherService::class.java, mapOf(this.resources.
getString(R.string.tunnel_extras_key) to
tunnelConfig))
}
private fun stopWatcherService() {
ServiceTracker.actionOnService(
Action.STOP, this,
WireGuardConnectivityWatcherService::class.java)
}
private fun attemptWatcherServiceToggle(tunnelConfig : String) {
scope.launch {
@ -132,11 +100,7 @@ class TunnelControlTile : TileService() {
if (!settings.isNullOrEmpty()) {
val setting = settings.first()
if(setting.isAutoTunnelEnabled) {
when(ServiceTracker.getServiceState( this@TunnelControlTile,
WireGuardConnectivityWatcherService::class.java,)) {
ServiceState.STARTED -> stopWatcherService()
ServiceState.STOPPED -> startWatcherService(tunnelConfig)
}
ServiceManager.toggleWatcherService(this@TunnelControlTile, tunnelConfig)
}
}
}

View File

@ -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)
)
}

View File

@ -1,6 +1,5 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.config
import android.content.pm.PackageManager
import android.widget.Toast
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
@ -25,7 +24,6 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
@ -45,6 +43,7 @@ import androidx.navigation.NavController
import com.google.accompanist.drawablepainter.DrawablePainter
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.Routes
import com.zaneschepke.wireguardautotunnel.ui.common.SearchBar
import kotlinx.coroutines.launch
@OptIn(ExperimentalComposeUiApi::class)
@ -71,7 +70,7 @@ fun ConfigScreen(
LaunchedEffect(Unit) {
viewModel.getTunnelById(id)
viewModel.emitAllInternetCapablePackages()
viewModel.emitQueriedPackages("")
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 ->
Row(
verticalAlignment = Alignment.CenterVertically,

View File

@ -8,12 +8,14 @@ import android.os.Build
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.toMutableStateList
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@ -96,8 +98,12 @@ class ConfigViewModel @Inject constructor(private val application : Application,
}
}
suspend fun emitAllInternetCapablePackages() {
_packages.emit(getAllInternetCapablePackages())
fun emitQueriedPackages(query : String) {
viewModelScope.launch {
_packages.emit(getAllInternetCapablePackages().filter {
it.packageName.contains(query)
})
}
}
private fun getAllInternetCapablePackages() : List<PackageInfo> {

View File

@ -12,11 +12,10 @@ import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.repository.Repository
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.ServiceTracker
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
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.model.Settings
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) {
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) {
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) {
viewModelScope.launch {
if(tunnelRepo.count() == 1L) {
ServiceTracker.actionOnService( Action.STOP, application, WireGuardConnectivityWatcherService::class.java)
ServiceManager.stopWatcherService(application.applicationContext)
val settings = settingsRepo.getAll()
if(!settings.isNullOrEmpty()) {
val setting = settings[0]
@ -91,22 +84,23 @@ class MainViewModel @Inject constructor(private val application : Application,
}
}
tunnelRepo.delete(tunnel)
ShortcutsManager.removeTunnelShortcuts(application.applicationContext, tunnel)
}
}
fun onTunnelStart(tunnelConfig : TunnelConfig) = viewModelScope.launch {
ServiceTracker.actionOnService( Action.START, application, WireGuardTunnelService::class.java,
mapOf(application.resources.getString(R.string.tunnel_extras_key) to tunnelConfig.toString()))
ServiceManager.startVpnService(application.applicationContext, tunnelConfig.toString())
}
fun onTunnelStop() {
ServiceTracker.actionOnService( Action.STOP, application, WireGuardTunnelService::class.java)
ServiceManager.stopVpnService(application.applicationContext)
}
suspend fun onTunnelQRSelected() {
codeScanner.scan().collect {
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))) {
showSnackBarMessage(application.resources.getString(R.string.barcode_downloading_message))
} else {
@ -130,9 +124,7 @@ class MainViewModel @Inject constructor(private val application : Application,
val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8)
val config = Config.parse(bufferReader)
val tunnelName = getNameFromFileName(fileName)
viewModelScope.launch {
tunnelRepo.save(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString()))
}
saveTunnel(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString()))
stream.close()
} catch(_: BadConfigException) {
viewModelScope.launch {
@ -141,6 +133,13 @@ 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")
private fun getFileName(context: Context, uri: Uri): String {
if (uri.scheme == "content") {

View File

@ -7,9 +7,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceTracker
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.ui.ViewState
@ -77,32 +75,18 @@ class SettingsViewModel @Inject constructor(private val application : Applicatio
return
}
if(_settings.value.isAutoTunnelEnabled) {
actionOnWatcherService(Action.STOP)
ServiceManager.stopWatcherService(application)
} else {
actionOnWatcherService(Action.START)
if(_settings.value.defaultTunnel != null) {
val defaultTunnel = _settings.value.defaultTunnel
ServiceManager.startWatcherService(application, defaultTunnel!!)
}
}
settingsRepo.save(_settings.value.copy(
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) {
_viewState.emit(_viewState.value.copy(
showSnackbarMessage = true,

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>

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>

View File

@ -86,4 +86,7 @@
<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>
</resources>