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