From 20cc2c09b0e827ec47c3a04a4121af95137e5b62 Mon Sep 17 00:00:00 2001 From: Zane Schepke Date: Mon, 4 Sep 2023 02:42:49 -0400 Subject: [PATCH] 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 --- app/build.gradle.kts | 4 +- app/src/main/AndroidManifest.xml | 8 +- .../receiver/BootReceiver.kt | 12 +-- .../receiver/NotificationActionReceiver.kt | 23 +----- .../service/foreground/ServiceManager.kt | 82 +++++++++++++++++++ .../service/foreground/ServiceTracker.kt | 58 ------------- .../WireGuardConnectivityWatcherService.kt | 30 ++----- .../service/shortcut/ShortcutsActivity.kt | 28 +++++++ .../service/shortcut/ShortcutsManager.kt | 73 +++++++++++++++++ .../service/{ => tile}/TunnelControlTile.kt | 46 ++--------- .../ui/common/SearchBar.kt | 80 ++++++++++++++++++ .../ui/screens/config/ConfigScreen.kt | 15 +++- .../ui/screens/config/ConfigViewModel.kt | 10 ++- .../ui/screens/main/MainViewModel.kt | 37 ++++----- .../ui/screens/settings/SettingsViewModel.kt | 28 ++----- app/src/main/res/drawable/vpn_off.xml | 5 ++ app/src/main/res/drawable/vpn_on.xml | 5 ++ app/src/main/res/values/strings.xml | 3 + 18 files changed, 346 insertions(+), 201 deletions(-) create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceManager.kt delete mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceTracker.kt create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsManager.kt rename app/src/main/java/com/zaneschepke/wireguardautotunnel/service/{ => tile}/TunnelControlTile.kt (69%) create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/SearchBar.kt create mode 100644 app/src/main/res/drawable/vpn_off.xml create mode 100644 app/src/main/res/drawable/vpn_on.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9c2f84f..fda21cf 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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 { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 716f987..f674159 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -58,6 +58,10 @@ + @@ -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"> @@ -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"> diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt index db1c71b..03b8e9f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt @@ -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 { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/NotificationActionReceiver.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/NotificationActionReceiver.kt index d8cdf9a..93a10ae 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/NotificationActionReceiver.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/NotificationActionReceiver.kt @@ -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 { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceManager.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceManager.kt new file mode 100644 index 0000000..960f0e8 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceManager.kt @@ -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 Context.isServiceRunning(service: Class) = + (getSystemService(ACTIVITY_SERVICE) as ActivityManager) + .getRunningServices(Integer.MAX_VALUE) + .any { it.service.className == service.name } + + fun getServiceState(context: Context, cls : Class): ServiceState { + val isServiceRunning = context.isServiceRunning(cls) + return if(isServiceRunning) ServiceState.STARTED else ServiceState.STOPPED + } + + private fun actionOnService(action: Action, context: Context, cls : Class, extras : Map? = 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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceTracker.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceTracker.kt deleted file mode 100644 index 64e5ceb..0000000 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceTracker.kt +++ /dev/null @@ -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 Context.isServiceRunning(service: Class) = - (getSystemService(ACTIVITY_SERVICE) as ActivityManager) - .getRunningServices(Integer.MAX_VALUE) - .any { it.service.className == service.name } - - fun getServiceState(context: Context, cls : Class): ServiceState { - val isServiceRunning = context.isServiceRunning(cls) - return if(isServiceRunning) ServiceState.STARTED else ServiceState.STOPPED - } - - fun actionOnService(action: Action, application: Application, cls : Class, extras : Map? = 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 actionOnService(action: Action, context: Context, cls : Class, extras : Map? = 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) } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt index 30354b2..4dac1a2 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt @@ -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 - ) - } } \ No newline at end of file diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt new file mode 100644 index 0000000..91c0940 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt @@ -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() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsManager.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsManager.kt new file mode 100644 index 0000000..04c0e9d --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsManager.kt @@ -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) : 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) : 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) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/TunnelControlTile.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt similarity index 69% rename from app/src/main/java/com/zaneschepke/wireguardautotunnel/service/TunnelControlTile.kt rename to app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt index d77fdd4..aa211de 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/TunnelControlTile.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt @@ -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) } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/SearchBar.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/SearchBar.kt new file mode 100644 index 0000000..7deea5a --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/SearchBar.kt @@ -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) + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt index 985386e..bc38ed4 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt @@ -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, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt index 8d57350..ce57f4b 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt @@ -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 { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt index 6897cff..8caed38 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt @@ -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") { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt index b8a3505..256fb7d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt @@ -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, diff --git a/app/src/main/res/drawable/vpn_off.xml b/app/src/main/res/drawable/vpn_off.xml new file mode 100644 index 0000000..d33949b --- /dev/null +++ b/app/src/main/res/drawable/vpn_off.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/vpn_on.xml b/app/src/main/res/drawable/vpn_on.xml new file mode 100644 index 0000000..1339fb3 --- /dev/null +++ b/app/src/main/res/drawable/vpn_on.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b84d101..14df420 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -86,4 +86,7 @@ Request Toggle VPN No tunnels available + Search packages + Clear Icon + Search Icon \ No newline at end of file