Compare commits

..

8 Commits

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

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

Fix shortcuts name not updating with config edits.

Bump versions.

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

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

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

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

Add package search for split tunneling.

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

Refactor and code cleanup.

Closes #23 Closes #21
2023-09-04 02:42:49 -04:00
26 changed files with 507 additions and 261 deletions
+4 -1
View File
@@ -13,6 +13,7 @@ WG Tunnel
[![Google Play](https://img.shields.io/badge/Google_Play-414141?style=for-the-badge&logo=google-play&logoColor=white)](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel)
[![Fire TV](https://img.shields.io/badge/fire%20tv-fc3b2d?style=for-the-badge&logo=amazon%20fire%20tv&logoColor=white)](https://www.amazon.com/gp/product/B0CFGGL7WK)
</span>
@@ -50,8 +51,10 @@ The inspiration for this app came from the inconvenience of constantly having to
* Add tunnels via .conf file
* Auto connect to VPN based on Wi-Fi SSID
* Split tunneling by application
* Split tunneling by application with search
* Always-on VPN for Android support
* Quick tile support for vpn toggling
* Dynamic shortcuts support for automation integration
* Configurable Trusted Network list
* Optional auto connect on mobile data
* Automatic service restart after reboot
+8 -8
View File
@@ -16,13 +16,13 @@ android {
compileSdk = 34
val versionMajor = 2
val versionMinor = 3
val versionPatch = 7
val versionMinor = 4
val versionPatch = 5
val versionBuild = 0
defaultConfig {
applicationId = "com.zaneschepke.wireguardautotunnel"
minSdk = 28
minSdk = 26
targetSdk = 34
versionCode = versionMajor * 10000 + versionMinor * 1000 + versionPatch * 100 + versionBuild
versionName = "${versionMajor}.${versionMinor}.${versionPatch}"
@@ -64,8 +64,8 @@ android {
}
dependencies {
implementation("androidx.core:core-ktx:1.10.1")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
implementation("androidx.activity:activity-compose:1.7.2")
implementation(platform("androidx.compose:compose-bom:2023.03.00"))
implementation("androidx.compose.ui:ui")
@@ -89,7 +89,7 @@ dependencies {
implementation("com.jakewharton.timber:timber:5.0.1")
// compose navigation
implementation("androidx.navigation:navigation-compose:2.7.1")
implementation("androidx.navigation:navigation-compose:2.7.2")
implementation("androidx.hilt:hilt-navigation-compose:1.0.0")
// hilt
@@ -107,10 +107,10 @@ dependencies {
implementation("io.objectbox:objectbox-kotlin:${rExtra.get("objectBoxVersion")}")
//lifecycle
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.1")
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.2")
//icons
implementation("androidx.compose.material:material-icons-extended:1.5.0")
implementation("androidx.compose.material:material-icons-extended:1.5.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
+7 -1
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">
@@ -4,6 +4,7 @@ import android.app.Application
import android.content.Context
import android.content.pm.PackageManager
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.mlkit.common.MlKit
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import dagger.hilt.android.HiltAndroidApp
@@ -22,6 +23,11 @@ class WireGuardAutoTunnel : Application() {
FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(false);
Timber.plant(Timber.DebugTree())
}
try {
MlKit.initialize(this)
} catch (e : Exception) {
Timber.e(e.message)
}
settingsRepo.init()
}
@@ -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 {
@@ -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 {
@@ -2,5 +2,6 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
enum class Action {
START,
START_FOREGROUND,
STOP
}
@@ -22,7 +22,7 @@ open class ForegroundService : Service() {
val action = intent.action
Timber.d("using an intent with action $action")
when (action) {
Action.START.name -> startService(intent.extras)
Action.START.name, Action.START_FOREGROUND.name -> startService(intent.extras)
Action.STOP.name -> stopService(intent.extras)
"android.net.VpnService" -> {
Timber.d("Always-on VPN starting service")
@@ -0,0 +1,110 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.ActivityManager
import android.app.Service
import android.content.Context
import android.content.Context.ACTIVITY_SERVICE
import android.content.Intent
import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase
import com.zaneschepke.wireguardautotunnel.R
object ServiceManager {
@Suppress("DEPRECATION")
private // Deprecated for third party Services.
fun <T> Context.isServiceRunning(service: Class<T>) =
(getSystemService(ACTIVITY_SERVICE) as ActivityManager)
.getRunningServices(Integer.MAX_VALUE)
.any { it.service.className == service.name }
fun <T : Service> getServiceState(context: Context, cls : Class<T>): ServiceState {
val isServiceRunning = context.isServiceRunning(cls)
return if(isServiceRunning) ServiceState.STARTED else ServiceState.STOPPED
}
private fun <T : Service> actionOnService(action: Action, context: Context, cls : Class<T>, extras : Map<String,String>? = null) {
if (getServiceState(context, cls) == ServiceState.STOPPED && action == Action.STOP) return
if (getServiceState(context, cls) == ServiceState.STARTED && action == Action.START) return
val intent = Intent(context, cls).also {
it.action = action.name
extras?.forEach {(k, v) ->
it.putExtra(k, v)
}
}
intent.component?.javaClass
try {
when(action) {
Action.START_FOREGROUND -> {
context.startForegroundService(intent)
}
Action.START -> {
context.startService(intent)
}
Action.STOP -> context.startService(intent)
}
} catch (e : Exception) {
e.message?.let { Firebase.crashlytics.log(it) }
}
}
fun startVpnService(context : Context, tunnelConfig : String) {
actionOnService(
Action.START,
context,
WireGuardTunnelService::class.java,
mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig))
}
fun stopVpnService(context : Context) {
actionOnService(
Action.STOP,
context,
WireGuardTunnelService::class.java
)
}
fun startVpnServiceForeground(context : Context, tunnelConfig : String) {
actionOnService(
Action.START_FOREGROUND,
context,
WireGuardTunnelService::class.java,
mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig))
}
private fun startWatcherServiceForeground(context : Context, tunnelConfig : String) {
actionOnService(
Action.START, context,
WireGuardConnectivityWatcherService::class.java, mapOf(context.
getString(R.string.tunnel_extras_key) to
tunnelConfig))
}
fun startWatcherService(context : Context, tunnelConfig : String) {
actionOnService(
Action.START, context,
WireGuardConnectivityWatcherService::class.java, mapOf(context.
getString(R.string.tunnel_extras_key) to
tunnelConfig))
}
fun stopWatcherService(context : Context) {
actionOnService(
Action.STOP, context,
WireGuardConnectivityWatcherService::class.java)
}
fun toggleWatcherService(context: Context, tunnelConfig : String) {
when(getServiceState( context,
WireGuardConnectivityWatcherService::class.java,)) {
ServiceState.STARTED -> stopWatcherService(context)
ServiceState.STOPPED -> startWatcherService(context, tunnelConfig)
}
}
fun toggleWatcherServiceForeground(context: Context, tunnelConfig : String) {
when(getServiceState( context,
WireGuardConnectivityWatcherService::class.java,)) {
ServiceState.STARTED -> stopWatcherService(context)
ServiceState.STOPPED -> startWatcherServiceForeground(context, tunnelConfig)
}
}
}
@@ -1,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) }
}
}
}
@@ -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,23 +53,30 @@ 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;
override fun onCreate() {
super.onCreate()
CoroutineScope(Dispatchers.Main).launch {
launchWatcherNotification()
}
}
override fun startService(extras: Bundle?) {
super.startService(extras)
launchWatcherNotification()
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)
@@ -182,41 +188,26 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
}
private suspend fun manageVpn() {
while(watcherJob.isActive) {
while(true) {
if(setting.isTunnelOnMobileDataEnabled &&
!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
)
}
}
@@ -37,8 +37,16 @@ class WireGuardTunnelService : ForegroundService() {
private var tunnelName : String = ""
override fun onCreate() {
super.onCreate()
CoroutineScope(Dispatchers.Main).launch {
launchVpnStartingNotification()
}
}
override fun startService(extras : Bundle?) {
super.startService(extras)
launchVpnStartingNotification()
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
cancelJob()
job = CoroutineScope(Dispatchers.IO).launch {
@@ -98,7 +106,7 @@ class WireGuardTunnelService : ForegroundService() {
override fun stopService(extras : Bundle?) {
super.stopService(extras)
CoroutineScope(Dispatchers.IO).launch() {
CoroutineScope(Dispatchers.IO).launch {
vpnService.stopTunnel()
}
cancelJob()
@@ -117,6 +125,18 @@ class WireGuardTunnelService : ForegroundService() {
super.startForeground(foregroundId, notification)
}
private fun launchVpnStartingNotification() {
val notification = notificationService.createNotification(
channelId = getString(R.string.vpn_channel_id),
channelName = getString(R.string.vpn_channel_name),
title = getString(R.string.vpn_starting),
onGoing = false,
showTimestamp = true,
description = getString(R.string.attempt_connection)
)
super.startForeground(foregroundId, notification)
}
private fun launchVpnConnectionFailedNotification(message : String) {
val notification = notificationService.createNotification(
channelId = getString(R.string.vpn_channel_id),
@@ -0,0 +1,53 @@
package com.zaneschepke.wireguardautotunnel.service.shortcut
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
class ShortcutsActivity : AppCompatActivity() {
@Inject
lateinit var settingsRepo : Repository<Settings>
private val scope = CoroutineScope(Dispatchers.Main);
private fun attemptWatcherServiceToggle(tunnelConfig : String) {
scope.launch {
val settings = settingsRepo.getAll()
if (!settings.isNullOrEmpty()) {
val setting = settings.first()
if(setting.isAutoTunnelEnabled) {
ServiceManager.toggleWatcherServiceForeground(this@ShortcutsActivity, tunnelConfig)
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if(intent.getStringExtra(ShortcutsManager.CLASS_NAME_EXTRA_KEY)
.equals(WireGuardTunnelService::class.java.name)) {
intent.getStringExtra(getString(R.string.tunnel_extras_key))?.let {
attemptWatcherServiceToggle(it)
}
when(intent.action){
Action.STOP.name -> ServiceManager.stopVpnService(this)
Action.START.name -> intent.getStringExtra(getString(R.string.tunnel_extras_key))
?.let { ServiceManager.startVpnService(this, it) }
}
}
finish()
}
}
@@ -0,0 +1,73 @@
package com.zaneschepke.wireguardautotunnel.service.shortcut
import android.content.Context
import android.content.Intent
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
object ShortcutsManager {
private const val SHORT_LABEL_MAX_SIZE = 10;
private const val LONG_LABEL_MAX_SIZE = 25;
private const val APPEND_ON = " On";
private const val APPEND_OFF = " Off"
const val CLASS_NAME_EXTRA_KEY = "className"
private fun createAndPushShortcut(context : Context, intent : Intent, id : String, shortLabel : String,
longLabel : String, drawable : Int ) {
val shortcut = ShortcutInfoCompat.Builder(context, id)
.setShortLabel(shortLabel)
.setLongLabel(longLabel)
.setIcon(IconCompat.createWithResource(context, drawable))
.setIntent(intent)
.build()
ShortcutManagerCompat.pushDynamicShortcut(context, shortcut)
}
fun createTunnelShortcuts(context : Context, tunnelConfig : TunnelConfig) {
createAndPushShortcut(context,
createTunnelOnIntent(context, mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig.toString())),
tunnelConfig.id.toString() + APPEND_ON,
tunnelConfig.name.take((SHORT_LABEL_MAX_SIZE - APPEND_ON.length)) + APPEND_ON,
tunnelConfig.name.take((LONG_LABEL_MAX_SIZE - APPEND_ON.length)) + APPEND_ON,
R.drawable.vpn_on
)
createAndPushShortcut(context,
createTunnelOffIntent(context, mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig.toString())),
tunnelConfig.id.toString() + APPEND_OFF,
tunnelConfig.name.take((SHORT_LABEL_MAX_SIZE - APPEND_OFF.length)) + APPEND_OFF,
tunnelConfig.name.take((LONG_LABEL_MAX_SIZE - APPEND_OFF.length)) + APPEND_OFF,
R.drawable.vpn_off
)
}
fun removeTunnelShortcuts(context : Context, tunnelConfig : TunnelConfig) {
ShortcutManagerCompat.removeDynamicShortcuts(context, listOf(tunnelConfig.id.toString() + APPEND_ON,
tunnelConfig.id.toString() + APPEND_OFF ))
}
private fun createTunnelOnIntent(context: Context, extras : Map<String,String>) : Intent {
return Intent(context, ShortcutsActivity::class.java).also {
it.action = Action.START.name
it.putExtra(CLASS_NAME_EXTRA_KEY, WireGuardTunnelService::class.java.name)
extras.forEach {(k, v) ->
it.putExtra(k, v)
}
}
}
private fun createTunnelOffIntent(context : Context, extras : Map<String,String>) : Intent {
return Intent(context, ShortcutsActivity::class.java).also {
it.action = Action.STOP.name
it.putExtra(CLASS_NAME_EXTRA_KEY, WireGuardTunnelService::class.java.name)
extras.forEach {(k, v) ->
it.putExtra(k, v)
}
}
}
}
@@ -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.startVpnServiceForeground(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.toggleWatcherServiceForeground(this@TunnelControlTile, tunnelConfig)
}
}
}
@@ -0,0 +1,80 @@
package com.zaneschepke.wireguardautotunnel.ui.common
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Clear
import androidx.compose.material.icons.rounded.Search
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import com.zaneschepke.wireguardautotunnel.R
@Composable
fun SearchBar(
onQuery : (queryString : String) -> Unit
) {
// Immediately update and keep track of query from text field changes.
var query: String by rememberSaveable { mutableStateOf("") }
var showClearIcon by rememberSaveable { mutableStateOf(false) }
if (query.isEmpty()) {
showClearIcon = false
} else if (query.isNotEmpty()) {
showClearIcon = true
}
TextField(
value = query,
onValueChange = { onQueryChanged ->
// If user makes changes to text, immediately updated it.
query = onQueryChanged
onQuery(onQueryChanged)
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Search,
tint = MaterialTheme.colorScheme.onBackground,
contentDescription = stringResource(id = R.string.search_icon)
)
},
trailingIcon = {
if (showClearIcon) {
IconButton(onClick = { query = "" }) {
Icon(
imageVector = Icons.Rounded.Clear,
tint = MaterialTheme.colorScheme.onBackground,
contentDescription = stringResource(id = R.string.clear_icon)
)
}
}
},
maxLines = 1,
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
disabledContainerColor = Color.Transparent,
),
placeholder = { Text(text = stringResource(R.string.hint_search_packages)) },
textStyle = MaterialTheme.typography.bodySmall,
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
modifier = Modifier
.fillMaxWidth()
.background(color = MaterialTheme.colorScheme.background, shape = RectangleShape)
)
}
@@ -1,6 +1,5 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.config
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,
@@ -8,12 +8,15 @@ 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.shortcut.ShortcutsManager
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 +99,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> {
@@ -108,12 +115,14 @@ class ConfigViewModel @Inject constructor(private val application : Application,
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageManager.getPackagesHoldingPermissions(permissions, PackageManager.PackageInfoFlags.of(0L))
} else {
@Suppress("DEPRECATION")
packageManager.getPackagesHoldingPermissions(permissions, 0)
}
}
suspend fun onSaveAllChanges() {
if(_tunnel.value != null) {
ShortcutsManager.removeTunnelShortcuts(application, _tunnel.value!!)
}
var wgQuick = _tunnel.value?.wgQuick
if(wgQuick != null) {
wgQuick = if(_include.value) {
@@ -129,6 +138,7 @@ class ConfigViewModel @Inject constructor(private val application : Application,
wgQuick = wgQuick
)?.let {
tunnelRepo.save(it)
ShortcutsManager.createTunnelShortcuts(application, it)
val settings = settingsRepo.getAll()
if(settings != null) {
val setting = settings[0]
@@ -1,6 +1,7 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main
import android.annotation.SuppressLint
import android.content.Intent
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility
@@ -17,7 +18,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
@@ -103,6 +103,7 @@ fun MainScreen(
val state by viewModel.state.collectAsStateWithLifecycle(Tunnel.State.DOWN)
val tunnelName by viewModel.tunnelName.collectAsStateWithLifecycle("")
// Nested scroll for control FAB
val nestedScrollConnection = remember {
object : NestedScrollConnection {
@@ -135,11 +136,9 @@ fun MainScreen(
}
val pickFileLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.GetContent()
) { file ->
if (file != null) {
viewModel.onTunnelFileSelected(file)
}
ActivityResultContracts.StartActivityForResult()
) { result ->
result.data?.data?.let { viewModel.onTunnelFileSelected(it) }
}
Scaffold(
@@ -196,7 +195,11 @@ fun MainScreen(
.fillMaxWidth()
.clickable {
showBottomSheet = false
pickFileLauncher.launch("*/*")
val fileSelectionIntent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
}
pickFileLauncher.launch(fileSelectionIntent)
}
.padding(10.dp)
) {
@@ -242,10 +245,11 @@ fun MainScreen(
) {
LazyColumn(
modifier = Modifier.fillMaxSize()
modifier = Modifier
.fillMaxSize()
.nestedScroll(nestedScrollConnection),
) {
itemsIndexed(tunnels.toList()) { index, tunnel ->
itemsIndexed(tunnels.toList()) { _, tunnel ->
val focusRequester = FocusRequester();
RowListItem(leadingIcon = Icons.Rounded.Circle,
leadingIconColor = if (tunnelName == tunnel.name) when (handshakeStatus) {
@@ -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") {
@@ -96,8 +96,6 @@ fun SettingsScreen(
val settings by viewModel.settings.collectAsStateWithLifecycle()
val trustedSSIDs by viewModel.trustedSSIDs.collectAsStateWithLifecycle()
val tunnels by viewModel.tunnels.collectAsStateWithLifecycle(mutableListOf())
val backgroundLocationState =
rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
var currentText by remember { mutableStateOf("") }
val scrollState = rememberScrollState()
@@ -135,41 +133,44 @@ fun SettingsScreen(
context.startActivity(intentSettings)
}
}
if(!backgroundLocationState.status.isGranted && Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
Column(horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.padding(padding)) {
Icon(Icons.Rounded.LocationOff, contentDescription = stringResource(id = R.string.map), modifier = Modifier
.padding(30.dp)
.size(128.dp))
Text(stringResource(R.string.prominent_background_location_title), textAlign = TextAlign.Center, modifier = Modifier.padding(30.dp), fontSize = 20.sp)
Text(stringResource(R.string.prominent_background_location_message), textAlign = TextAlign.Center, modifier = Modifier.padding(30.dp), fontSize = 15.sp)
Row(
modifier = if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) Modifier
.fillMaxWidth()
.padding(10.dp) else Modifier
.fillMaxWidth()
.padding(30.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly
) {
Button(onClick = {
navController.navigate(Routes.Main.name)
}) {
Text(stringResource(id = R.string.no_thanks))
}
Button(modifier = Modifier.focusRequester(focusRequester), onClick = {
openSettings()
}) {
Text(stringResource(id = R.string.turn_on))
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val backgroundLocationState =
rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
if(!backgroundLocationState.status.isGranted) {
Column(horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.padding(padding)) {
Icon(Icons.Rounded.LocationOff, contentDescription = stringResource(id = R.string.map), modifier = Modifier
.padding(30.dp)
.size(128.dp))
Text(stringResource(R.string.prominent_background_location_title), textAlign = TextAlign.Center, modifier = Modifier.padding(30.dp), fontSize = 20.sp)
Text(stringResource(R.string.prominent_background_location_message), textAlign = TextAlign.Center, modifier = Modifier.padding(30.dp), fontSize = 15.sp)
Row(
modifier = if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) Modifier
.fillMaxWidth()
.padding(10.dp) else Modifier
.fillMaxWidth()
.padding(30.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly
) {
Button(onClick = {
navController.navigate(Routes.Main.name)
}) {
Text(stringResource(id = R.string.no_thanks))
}
Button(modifier = Modifier.focusRequester(focusRequester), onClick = {
openSettings()
}) {
Text(stringResource(id = R.string.turn_on))
}
}
}
return
}
return
}
if(!fineLocationState.status.isGranted) {
@@ -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,
+5
View File
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M20.83,18H21v-4h2v-4H12.83L20.83,18zM19.78,22.61l1.41,-1.41L2.81,2.81L1.39,4.22l2.59,2.59C2.2,7.85 1,9.79 1,12c0,3.31 2.69,6 6,6c2.21,0 4.15,-1.2 5.18,-2.99L19.78,22.61zM8.99,11.82C9,11.88 9,11.94 9,12c0,1.1 -0.9,2 -2,2s-2,-0.9 -2,-2s0.9,-2 2,-2c0.06,0 0.12,0 0.18,0.01L8.99,11.82z"/>
</vector>
+5
View File
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12.65,10C11.83,7.67 9.61,6 7,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6c2.61,0 4.83,-1.67 5.65,-4H17v4h4v-4h2v-4H12.65zM7,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z"/>
</vector>
+5
View File
@@ -86,4 +86,9 @@
<string name="request">Request</string>
<string name="toggle_vpn">Toggle VPN</string>
<string name="no_tunnel_available">No tunnels available</string>
<string name="hint_search_packages">Search packages</string>
<string name="clear_icon">Clear Icon</string>
<string name="search_icon">Search Icon</string>
<string name="attempt_connection">Attempting connection..</string>
<string name="vpn_starting">VPN Starting</string>
</resources>
+5 -5
View File
@@ -1,20 +1,20 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
val objectBoxVersion by extra("3.5.1")
val hiltVersion by extra("2.47")
val objectBoxVersion by extra("3.7.0")
val hiltVersion by extra("2.48")
val accompanistVersion by extra("0.31.2-alpha")
dependencies {
classpath("io.objectbox:objectbox-gradle-plugin:$objectBoxVersion")
classpath("com.google.gms:google-services:4.3.15")
classpath("com.google.firebase:firebase-crashlytics-gradle:2.9.7")
classpath("com.google.firebase:firebase-crashlytics-gradle:2.9.9")
}
}
plugins {
id("com.android.application") version "8.2.0-beta01" apply false
id("com.android.application") version "8.2.0-beta03" apply false
id("org.jetbrains.kotlin.android") version "1.8.22" apply false
id("com.google.dagger.hilt.android") version "2.44" apply false
id("com.google.dagger.hilt.android") version "2.48" apply false
kotlin("plugin.serialization") version "1.8.22" apply false
}