Compare commits

..

10 Commits

Author SHA1 Message Date
Zane Schepke ee8db0a859 fix: ui bug and graphene notification
Fixes a bug where save button was hidden on config screen

Adds a disclaimer notification for when GrapheneOS auto enabled Always-on VPN on first app tunnel start

Closes #121 #120
2024-02-20 16:17:34 -05:00
Zane Schepke c8205c4c59 fix: ui tunnel display bug
Fixes a bug where turning on auto tunneling hides the first tunnel in the app.

Closes #116
2024-02-19 08:45:52 -05:00
Zane Schepke 3247e94358 Merge pull request #105 from zaneschepke/dependabot/github_actions/actions/upload-artifact-4.3.1
build(deps): bump actions/upload-artifact from 4.3.0 to 4.3.1
2024-02-19 06:59:32 -05:00
Zane Schepke 2690ce29e1 feat: migrate to forked lib
Migrated app to a forked version of wireguard-android to enable development work on features that require changes to the core lib, like #107 #104 #87 #52 #6

Improved first launch flow by change vpn permission to only launch on first tunnel start

Changed to proper database seeding strategy

Updated README to account for GitHub packages auth requirement

Migrated from deprecated UI components and libs

Bump versions
2024-02-18 23:28:06 -05:00
Zane Schepke 500b85f687 update gradle, vpn permission 2024-02-09 22:34:10 -05:00
dependabot[bot] 84b2b75271 build(deps): bump actions/upload-artifact from 4.3.0 to 4.3.1
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.3.0 to 4.3.1.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4.3.0...v4.3.1)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-06 14:02:45 +00:00
Zane Schepke 0197198f7b Merge pull request #100 from zaneschepke/dependabot/github_actions/peter-evans/repository-dispatch-3
build(deps): bump peter-evans/repository-dispatch from 2 to 3
2024-01-27 12:59:38 -05:00
Zane Schepke 0b271778c9 Merge pull request #99 from zaneschepke/dependabot/github_actions/actions/upload-artifact-4.3.0
build(deps): bump actions/upload-artifact from 4.2.0 to 4.3.0
2024-01-27 12:59:13 -05:00
dependabot[bot] 097097f620 build(deps): bump peter-evans/repository-dispatch from 2 to 3
Bumps [peter-evans/repository-dispatch](https://github.com/peter-evans/repository-dispatch) from 2 to 3.
- [Release notes](https://github.com/peter-evans/repository-dispatch/releases)
- [Commits](https://github.com/peter-evans/repository-dispatch/compare/v2...v3)

---
updated-dependencies:
- dependency-name: peter-evans/repository-dispatch
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-25 13:27:12 +00:00
dependabot[bot] 20dfaed8de build(deps): bump actions/upload-artifact from 4.2.0 to 4.3.0
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.2.0 to 4.3.0.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4.2.0...v4.3.0)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-24 13:47:56 +00:00
30 changed files with 481 additions and 311 deletions
+3 -1
View File
@@ -19,6 +19,8 @@ jobs:
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }} SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
KEY_STORE_FILE: 'android_keystore.jks' KEY_STORE_FILE: 'android_keystore.jks'
KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/ KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/
GH_USER: ${{ secrets.GH_USER }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -66,7 +68,7 @@ jobs:
echo "VERSION_CODE=$version_code" >> $GITHUB_ENV echo "VERSION_CODE=$version_code" >> $GITHUB_ENV
# Save the APK after the Build job is complete to publish it as a Github release in the next job # Save the APK after the Build job is complete to publish it as a Github release in the next job
- name: Upload APK - name: Upload APK
uses: actions/upload-artifact@v4.2.0 uses: actions/upload-artifact@v4.3.1
with: with:
name: wgtunnel name: wgtunnel
path: ${{ steps.apk-path.outputs.path }} path: ${{ steps.apk-path.outputs.path }}
+4 -2
View File
@@ -20,6 +20,8 @@ jobs:
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }} SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
KEY_STORE_FILE: 'android_keystore.jks' KEY_STORE_FILE: 'android_keystore.jks'
KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/ KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/
GH_USER: ${{ secrets.GH_USER }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -67,7 +69,7 @@ jobs:
echo "VERSION_CODE=$version_code" >> $GITHUB_ENV echo "VERSION_CODE=$version_code" >> $GITHUB_ENV
# Save the APK after the Build job is complete to publish it as a Github release in the next job # Save the APK after the Build job is complete to publish it as a Github release in the next job
- name: Upload APK - name: Upload APK
uses: actions/upload-artifact@v4.2.0 uses: actions/upload-artifact@v4.3.1
with: with:
name: wgtunnel name: wgtunnel
path: ${{ steps.apk-path.outputs.path }} path: ${{ steps.apk-path.outputs.path }}
@@ -76,7 +78,7 @@ jobs:
with: with:
name: wgtunnel name: wgtunnel
- name: Repository Dispatch for my F-Droid repo - name: Repository Dispatch for my F-Droid repo
uses: peter-evans/repository-dispatch@v2 uses: peter-evans/repository-dispatch@v3
with: with:
token: ${{ secrets.PAT }} token: ${{ secrets.PAT }}
repository: zaneschepke/fdroid repository: zaneschepke/fdroid
+25
View File
@@ -71,6 +71,31 @@ The repository for these docs can be found [here](https://github.com/zaneschepke
``` ```
$ git clone https://github.com/zaneschepke/wgtunnel $ git clone https://github.com/zaneschepke/wgtunnel
$ cd wgtunnel $ cd wgtunnel
```
Create a personal access token (classic) in GitHuv to be able to pull the wireguard-android github dependencies from GitHub packages
as documented [here](https://docs.github.com/en/packages/learn-github-packages/introduction-to-github-packages#authenticating-to-github-packages).
Alternatively, you can clone [wireguard-android](https://github.com/zaneschepke/wireguard-android) and run the following command to publish the dependency to your local maven repository (requires you have maven installed). This is the ideal approach
if you intent to make changes to this lib.
```
$ git clone https://github.com/zaneschepke/wireguard-android
$ cd wireguard-android
$ brew install maven
$ ./gradlew publishToMavenLocal
```
The [wireguard-android](https://github.com/zaneschepke/wireguard-android) dependency is a fork of the official [wireguard-android](https://github.com/WireGuard/wireguard-android) library.
Add the following lines to local.properties file:
```
GH_USER=<your github username>
GH_TOKEN=<the personal access token with read package permission you just created>
```
And then build the app:
```
$ ./gradlew assembleDebug $ ./gradlew assembleDebug
``` ```
+2 -2
View File
@@ -156,13 +156,14 @@ dependencies {
debugImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.manifest) debugImplementation(libs.androidx.compose.manifest)
// wg // get tunnel lib from github packages or mavenLocal
implementation(libs.tunnel) implementation(libs.tunnel)
coreLibraryDesugaring(libs.desugar.jdk.libs) coreLibraryDesugaring(libs.desugar.jdk.libs)
// logging // logging
implementation(libs.timber) implementation(libs.timber)
// compose navigation // compose navigation
implementation(libs.androidx.navigation.compose) implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.hilt.navigation.compose) implementation(libs.androidx.hilt.navigation.compose)
@@ -172,7 +173,6 @@ dependencies {
ksp(libs.hilt.android.compiler) ksp(libs.hilt.android.compiler)
// accompanist // accompanist
implementation(libs.accompanist.systemuicontroller)
implementation(libs.accompanist.permissions) implementation(libs.accompanist.permissions)
implementation(libs.accompanist.flowlayout) implementation(libs.accompanist.flowlayout)
implementation(libs.accompanist.drawablepainter) implementation(libs.accompanist.drawablepainter)
@@ -4,6 +4,7 @@ import androidx.room.testing.MigrationTestHelper
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import com.zaneschepke.wireguardautotunnel.data.AppDatabase import com.zaneschepke.wireguardautotunnel.data.AppDatabase
import com.zaneschepke.wireguardautotunnel.data.Queries
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@@ -26,33 +27,7 @@ class MigrationTest {
helper.createDatabase(dbName, 4).apply { helper.createDatabase(dbName, 4).apply {
// Database has schema version 1. Insert some data using SQL queries. // Database has schema version 1. Insert some data using SQL queries.
// You can't use DAO classes because they expect the latest schema. // You can't use DAO classes because they expect the latest schema.
execSQL( execSQL(Queries.createDefaultSettings())
"INSERT INTO Settings (is_tunnel_enabled," +
"is_tunnel_on_mobile_data_enabled," +
"trusted_network_ssids," +
"default_tunnel," +
"is_always_on_vpn_enabled," +
"is_tunnel_on_ethernet_enabled," +
"is_shortcuts_enabled," +
"is_battery_saver_enabled," +
"is_tunnel_on_wifi_enabled," +
"is_kernel_enabled," +
"is_restore_on_boot_enabled," +
"is_multi_tunnel_enabled)" +
" VALUES " +
"('false'," +
"'false'," +
"'[trustedSSID1,trustedSSID2]'," +
"'defaultTunnel'," +
"'false'," +
"'false'," +
"'false'," +
"'false'," +
"'false'," +
"'false'," +
"'false'," +
"'false')",
)
execSQL( execSQL(
"INSERT INTO TunnelConfig (name, wg_quick)" + " VALUES ('hello', 'hello')", "INSERT INTO TunnelConfig (name, wg_quick)" + " VALUES ('hello', 'hello')",
) )
@@ -0,0 +1,21 @@
package com.zaneschepke.wireguardautotunnel.data
import androidx.room.RoomDatabase
import androidx.sqlite.db.SupportSQLiteDatabase
import timber.log.Timber
class DatabaseCallback : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) = db.run {
// Notice non-ui thread is here
beginTransaction()
try {
execSQL(Queries.createDefaultSettings())
Timber.i("Bootstrapping settings data")
setTransactionSuccessful()
} catch (e : Exception) {
Timber.e(e)
} finally {
endTransaction()
}
}
}
@@ -0,0 +1,33 @@
package com.zaneschepke.wireguardautotunnel.data
object Queries {
fun createDefaultSettings() : String {
return """
INSERT INTO Settings (is_tunnel_enabled,
is_tunnel_on_mobile_data_enabled,
trusted_network_ssids,
default_tunnel,
is_always_on_vpn_enabled,
is_tunnel_on_ethernet_enabled,
is_shortcuts_enabled,
is_battery_saver_enabled,
is_tunnel_on_wifi_enabled,
is_kernel_enabled,
is_restore_on_boot_enabled,
is_multi_tunnel_enabled)
VALUES
('false',
'false',
'sampleSSID1,sampleSSID2',
NULL,
'false',
'false',
'false',
'false',
'false',
'false',
'false',
'false')
""".trimIndent()
}
}
@@ -4,6 +4,7 @@ import android.content.Context
import androidx.room.Room import androidx.room.Room
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.AppDatabase import com.zaneschepke.wireguardautotunnel.data.AppDatabase
import com.zaneschepke.wireguardautotunnel.data.DatabaseCallback
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@@ -23,6 +24,7 @@ class DatabaseModule {
context.getString(R.string.db_name), context.getString(R.string.db_name),
) )
.fallbackToDestructiveMigration() .fallbackToDestructiveMigration()
.addCallback(DatabaseCallback())
.build() .build()
} }
} }
@@ -8,6 +8,7 @@ import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepositor
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.util.goAsync import com.zaneschepke.wireguardautotunnel.util.goAsync
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@@ -21,8 +22,10 @@ class BootReceiver : BroadcastReceiver() {
if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return@goAsync if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return@goAsync
val settings = settingsRepository.getSettings() val settings = settingsRepository.getSettings()
if (settings.isAutoTunnelEnabled) { if (settings.isAutoTunnelEnabled) {
Timber.i("Starting watcher service from boot")
ServiceManager.startWatcherServiceForeground(context!!) ServiceManager.startWatcherServiceForeground(context!!)
} else if(settings.isAlwaysOnVpnEnabled) { } else if(settings.isAlwaysOnVpnEnabled) {
Timber.i("Starting tunnel from boot")
ServiceManager.startVpnServicePrimaryTunnel(context!!, settings, tunnelConfigRepository.getAll().firstOrNull()) ServiceManager.startVpnServicePrimaryTunnel(context!!, settings, tunnelConfigRepository.getAll().firstOrNull())
} }
} }
@@ -152,12 +152,16 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
wakeLock = wakeLock =
(getSystemService(Context.POWER_SERVICE) as PowerManager).run { (getSystemService(Context.POWER_SERVICE) as PowerManager).run {
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply { newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
if (isBatterySaverOn) { try {
Timber.d("Initiating wakelock with timeout") if (isBatterySaverOn) {
acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT) Timber.d("Initiating wakelock with timeout")
} else { acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT)
Timber.d("Initiating wakelock with zero timeout") } else {
acquire(Constants.DEFAULT_WATCHER_WAKE_LOCK_TIMEOUT) Timber.d("Initiating wakelock with zero timeout")
acquire(Constants.DEFAULT_WATCHER_WAKE_LOCK_TIMEOUT)
}
} finally {
release()
} }
} }
} }
@@ -84,6 +84,8 @@ class WireGuardTunnelService : ForegroundService() {
tunnelName = tunnel.name tunnelName = tunnel.name
vpnService.startTunnel(tunnel) vpnService.startTunnel(tunnel)
} }
} else {
launchAlwaysOnDisabledNotification()
} }
} }
} }
@@ -116,6 +118,11 @@ class WireGuardTunnelService : ForegroundService() {
} }
} }
private fun launchAlwaysOnDisabledNotification() {
launchVpnNotification(title = this.getString(R.string.vpn_connection_failed),
description = this.getString(R.string.always_on_disabled))
}
override fun stopService(extras: Bundle?) { override fun stopService(extras: Bundle?) {
super.stopService(extras) super.stopService(extras)
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
@@ -25,7 +25,7 @@ import javax.inject.Inject
class WireGuardTunnel class WireGuardTunnel
@Inject @Inject
constructor( constructor(
@Userspace private val userspaceBackend: Backend, @Userspace private val userspaceBackend : Backend,
@Kernel private val kernelBackend: Backend, @Kernel private val kernelBackend: Backend,
private val settingsRepository: SettingsRepository private val settingsRepository: SettingsRepository
) : VpnService { ) : VpnService {
@@ -1,13 +1,12 @@
package com.zaneschepke.wireguardautotunnel.ui package com.zaneschepke.wireguardautotunnel.ui
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class ActivityViewModel class ActivityViewModel
@Inject @Inject
constructor( constructor() : ViewModel() {
private val settingsRepo: SettingsDao,
) : ViewModel() {} }
@@ -6,11 +6,13 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.Settings import android.provider.Settings
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.focusable import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarData import androidx.compose.material3.SnackbarData
@@ -20,13 +22,12 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
@@ -35,7 +36,6 @@ import androidx.navigation.compose.rememberNavController
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.rememberPermissionState
import com.wireguard.android.backend.GoBackend
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
@@ -47,7 +47,6 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
import com.zaneschepke.wireguardautotunnel.ui.theme.TransparentSystemBars
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@@ -70,58 +69,39 @@ class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge(navigationBarStyle = SystemBarStyle.dark(Color.Transparent.toArgb()))
// load preferences into memory and init data // load preferences into memory and init data
lifecycleScope.launch { lifecycleScope.launch {
try { try {
dataStoreManager.init() dataStoreManager.init()
if (settingsRepository.getAll().isEmpty()) {
settingsRepository.save(com.zaneschepke.wireguardautotunnel.data.model.Settings())
}
WireGuardAutoTunnel.requestTileServiceStateUpdate() WireGuardAutoTunnel.requestTileServiceStateUpdate()
} catch (e: IOException) { } catch (e: IOException) {
Timber.e("Failed to load preferences") Timber.e("Failed to load preferences")
} }
} }
setContent { setContent {
// val activityViewModel = hiltViewModel<ActivityViewModel>() //val activityViewModel = hiltViewModel<ActivityViewModel>()
val navController = rememberNavController() val navController = rememberNavController()
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
WireguardAutoTunnelTheme { WireguardAutoTunnelTheme {
TransparentSystemBars()
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
val notificationPermissionState = val notificationPermissionState = if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS) rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS) else null
fun requestNotificationPermission() { fun requestNotificationPermission() {
if ( if (notificationPermissionState != null && !notificationPermissionState.status.isGranted
!notificationPermissionState.status.isGranted &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
) { ) {
notificationPermissionState.launchPermissionRequest() notificationPermissionState.launchPermissionRequest()
} }
} }
var vpnIntent by remember { mutableStateOf(GoBackend.VpnService.prepare(this)) } LaunchedEffect(Unit) {
val vpnActivityResultState = requestNotificationPermission()
rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult(),
onResult = {
val accepted = (it.resultCode == RESULT_OK)
if (accepted) {
vpnIntent = null
}
},
)
LaunchedEffect(vpnIntent) {
if (vpnIntent != null) {
vpnActivityResultState.launch(vpnIntent)
} else {
requestNotificationPermission()
}
} }
fun showSnackBarMessage(message: String) { fun showSnackBarMessage(message: String) {
@@ -154,9 +134,11 @@ class MainActivity : AppCompatActivity() {
) )
} }
}, },
modifier = Modifier.focusable().focusProperties { up = focusRequester }, modifier = Modifier
.focusable()
.focusProperties { up = focusRequester },
bottomBar = bottomBar =
if (vpnIntent == null && notificationPermissionState.status.isGranted) { if (notificationPermissionState == null || notificationPermissionState.status.isGranted) {
{ {
BottomNavBar( BottomNavBar(
navController, navController,
@@ -171,78 +153,70 @@ class MainActivity : AppCompatActivity() {
{} {}
}, },
) { padding -> ) { padding ->
if (vpnIntent != null) { if (notificationPermissionState != null && !notificationPermissionState.status.isGranted) {
PermissionRequestFailedScreen( Column(modifier = Modifier.padding(padding)) {
padding = padding, PermissionRequestFailedScreen(
onRequestAgain = { vpnActivityResultState.launch(vpnIntent) }, onRequestAgain = {
message = getString(R.string.vpn_permission_required), val intentSettings =
getString(R.string.retry), Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
) intentSettings.data =
return@Scaffold Uri.fromParts(
} Constants.URI_PACKAGE_SCHEME,
if (!notificationPermissionState.status.isGranted) { this@MainActivity.packageName,
PermissionRequestFailedScreen( null,
padding = padding, )
onRequestAgain = { startActivity(intentSettings)
val intentSettings =
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intentSettings.data =
Uri.fromParts(
Constants.URI_PACKAGE_SCHEME,
this.packageName,
null,
)
startActivity(intentSettings)
},
message = getString(R.string.notification_permission_required),
getString(R.string.open_settings),
)
return@Scaffold
}
NavHost(navController, startDestination = Screen.Main.route) {
composable(
Screen.Main.route,
) {
MainScreen(
padding = padding,
focusRequester = focusRequester,
showSnackbarMessage = { message -> showSnackBarMessage(message) },
navController = navController,
)
}
composable(
Screen.Settings.route,
) {
SettingsScreen(
padding = padding,
showSnackbarMessage = { message -> showSnackBarMessage(message) },
focusRequester = focusRequester,
)
}
composable(
Screen.Support.route,
) {
SupportScreen(
padding = padding,
focusRequester = focusRequester,
showSnackbarMessage = { message -> showSnackBarMessage(message) },
)
}
composable("${Screen.Config.route}/{id}") {
val id = it.arguments?.getString("id")
if (!id.isNullOrBlank()) {
// https://dagger.dev/hilt/view-model#assisted-injection
ConfigScreen(
navController = navController,
id = id,
showSnackbarMessage = { message ->
showSnackBarMessage(message)
}, },
focusRequester = focusRequester, message = getString(R.string.notification_permission_required),
getString(R.string.open_settings),
) )
return@Scaffold
}
}
NavHost(navController, startDestination = Screen.Main.route) {
composable(
Screen.Main.route,
) {
MainScreen(
focusRequester = focusRequester,
showSnackbarMessage = { message -> showSnackBarMessage(message) },
navController = navController,
)
}
composable(
Screen.Settings.route,
) {
SettingsScreen(
padding = padding,
showSnackbarMessage = { message -> showSnackBarMessage(message) },
focusRequester = focusRequester,
)
//
}
composable(
Screen.Support.route,
) {
SupportScreen(
padding = padding,
focusRequester = focusRequester,
showSnackbarMessage = { message -> showSnackBarMessage(message) },
)
}
composable("${Screen.Config.route}/{id}") {
val id = it.arguments?.getString("id")
if (!id.isNullOrBlank()) {
ConfigScreen(
padding = padding,
navController = navController,
id = id,
showSnackbarMessage = { message ->
showSnackBarMessage(message)
},
focusRequester = focusRequester,
)
}
} }
} }
}
} }
} }
} }
@@ -2,7 +2,6 @@ package com.zaneschepke.wireguardautotunnel.ui.common
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button import androidx.compose.material3.Button
@@ -17,7 +16,6 @@ import kotlinx.coroutines.launch
@Composable @Composable
fun PermissionRequestFailedScreen( fun PermissionRequestFailedScreen(
padding: PaddingValues,
onRequestAgain: () -> Unit, onRequestAgain: () -> Unit,
message: String, message: String,
buttonText: String buttonText: String
@@ -26,7 +24,7 @@ fun PermissionRequestFailedScreen(
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxSize().padding(padding), modifier = Modifier.fillMaxSize(),
) { ) {
Text(message, textAlign = TextAlign.Center, modifier = Modifier.padding(15.dp)) Text(message, textAlign = TextAlign.Center, modifier = Modifier.padding(15.dp))
Button( Button(
@@ -19,7 +19,6 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
@@ -33,7 +32,6 @@ fun CustomSnackBar(
isRtl: Boolean = true, isRtl: Boolean = true,
containerColor: Color = MaterialTheme.colorScheme.surface containerColor: Color = MaterialTheme.colorScheme.surface
) { ) {
val context = LocalContext.current
Snackbar( Snackbar(
containerColor = containerColor, containerColor = containerColor,
modifier = modifier =
@@ -1,13 +1,13 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.config package com.zaneschepke.wireguardautotunnel.ui.screens.config
import android.annotation.SuppressLint import android.annotation.SuppressLint
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusGroup import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
@@ -29,7 +29,7 @@ import androidx.compose.material.icons.rounded.ContentCopy
import androidx.compose.material.icons.rounded.Delete import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.Refresh import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material.icons.rounded.Save import androidx.compose.material.icons.rounded.Save
import androidx.compose.material3.AlertDialog import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FabPosition import androidx.compose.material3.FabPosition
@@ -51,7 +51,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
@@ -63,6 +62,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
@@ -86,12 +86,11 @@ import kotlinx.coroutines.delay
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@OptIn( @OptIn(
ExperimentalComposeUiApi::class,
ExperimentalMaterial3Api::class, ExperimentalMaterial3Api::class,
ExperimentalFoundationApi::class,
) )
@Composable @Composable
fun ConfigScreen( fun ConfigScreen(
padding: PaddingValues,
viewModel: ConfigViewModel = hiltViewModel(), viewModel: ConfigViewModel = hiltViewModel(),
focusRequester: FocusRequester, focusRequester: FocusRequester,
navController: NavController, navController: NavController,
@@ -129,13 +128,16 @@ fun ConfigScreen(
val fillMaxWidth = .85f val fillMaxWidth = .85f
val screenPadding = 5.dp val screenPadding = 5.dp
val applicationButtonText = { val applicationButtonText = buildAnnotatedString {
"Tunneling apps: " + append(stringResource(id = R.string.tunneling_apps))
append(": ")
if (uiState.isAllApplicationsEnabled) { if (uiState.isAllApplicationsEnabled) {
"all" append(stringResource(id = R.string.all))
} else { } else {
"${uiState.checkedPackageNames.size} " + append("${uiState.checkedPackageNames.size} ")
(if (uiState.include) "included" else "excluded") (if (uiState.include) append(stringResource(id = R.string.included)) else append(
stringResource(id = R.string.excluded),
))
} }
} }
@@ -145,7 +147,7 @@ fun ConfigScreen(
showAuthPrompt = false showAuthPrompt = false
isAuthenticated = true isAuthenticated = true
}, },
onError = { error -> onError = {
showAuthPrompt = false showAuthPrompt = false
showSnackbarMessage(Event.Error.AuthenticationFailed.message) showSnackbarMessage(Event.Error.AuthenticationFailed.message)
}, },
@@ -161,20 +163,24 @@ fun ConfigScreen(
remember(uiState.packages) { remember(uiState.packages) {
uiState.packages.sortedBy { viewModel.getPackageLabel(it) } uiState.packages.sortedBy { viewModel.getPackageLabel(it) }
} }
AlertDialog(onDismissRequest = { showApplicationsDialog = false }) { BasicAlertDialog(onDismissRequest = { showApplicationsDialog = false }) {
Surface( Surface(
tonalElevation = 2.dp, tonalElevation = 2.dp,
shadowElevation = 2.dp, shadowElevation = 2.dp,
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface, color = MaterialTheme.colorScheme.surface,
modifier = modifier =
Modifier.fillMaxWidth() Modifier
.fillMaxHeight(if (uiState.isAllApplicationsEnabled) 1 / 5f else 4 / 5f), .fillMaxWidth()
.fillMaxHeight(if (uiState.isAllApplicationsEnabled) 1 / 5f else 4 / 5f),
) { ) {
Column(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier
.fillMaxWidth()) {
Row( Row(
modifier = modifier =
Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 7.dp), Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
@@ -187,8 +193,9 @@ fun ConfigScreen(
if (!uiState.isAllApplicationsEnabled) { if (!uiState.isAllApplicationsEnabled) {
Row( Row(
modifier = modifier =
Modifier.fillMaxWidth() Modifier
.padding(horizontal = 20.dp, vertical = 7.dp), .fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
@@ -219,8 +226,9 @@ fun ConfigScreen(
} }
Row( Row(
modifier = modifier =
Modifier.fillMaxWidth() Modifier
.padding(horizontal = 20.dp, vertical = 7.dp), .fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
@@ -236,7 +244,9 @@ fun ConfigScreen(
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxSize().padding(5.dp), modifier = Modifier
.fillMaxSize()
.padding(5.dp),
) { ) {
Row(modifier = Modifier.fillMaxWidth(fillMaxWidth)) { Row(modifier = Modifier.fillMaxWidth(fillMaxWidth)) {
val drawable = val drawable =
@@ -262,9 +272,9 @@ fun ConfigScreen(
Checkbox( Checkbox(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
checked = checked =
(uiState.checkedPackageNames.contains( (uiState.checkedPackageNames.contains(
pack.packageName pack.packageName
)), )),
onCheckedChange = { onCheckedChange = {
if (it) { if (it) {
viewModel.onAddCheckedPackage(pack.packageName) viewModel.onAddCheckedPackage(pack.packageName)
@@ -279,7 +289,9 @@ fun ConfigScreen(
} }
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxSize().padding(top = 5.dp), modifier = Modifier
.fillMaxSize()
.padding(top = 5.dp),
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
) { ) {
TextButton(onClick = { showApplicationsDialog = false }) { TextButton(onClick = { showApplicationsDialog = false }) {
@@ -331,7 +343,11 @@ fun ConfigScreen(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
modifier = modifier =
Modifier.verticalScroll(rememberScrollState()).weight(1f, true).fillMaxSize(), Modifier
.verticalScroll(rememberScrollState())
.weight(1f, true)
.fillMaxSize()
.padding(padding),
) { ) {
Surface( Surface(
tonalElevation = 2.dp, tonalElevation = 2.dp,
@@ -340,16 +356,20 @@ fun ConfigScreen(
color = MaterialTheme.colorScheme.surface, color = MaterialTheme.colorScheme.surface,
modifier = modifier =
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { (if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Modifier.fillMaxHeight(fillMaxHeight).fillMaxWidth(fillMaxWidth) Modifier
.fillMaxHeight(fillMaxHeight)
.fillMaxWidth(fillMaxWidth)
} else { } else {
Modifier.fillMaxWidth(fillMaxWidth) Modifier.fillMaxWidth(fillMaxWidth)
}) })
.padding(top = 50.dp, bottom = 10.dp), .padding(bottom = 10.dp),
) { ) {
Column( Column(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
modifier = Modifier.padding(15.dp).focusGroup(), modifier = Modifier
.padding(15.dp)
.focusGroup(),
) { ) {
SectionTitle( SectionTitle(
stringResource(R.string.interface_), stringResource(R.string.interface_),
@@ -361,10 +381,14 @@ fun ConfigScreen(
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
label = stringResource(R.string.name), label = stringResource(R.string.name),
hint = stringResource(R.string.tunnel_name).lowercase(), hint = stringResource(R.string.tunnel_name).lowercase(),
modifier = Modifier.fillMaxWidth().focusRequester(focusRequester), modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
) )
OutlinedTextField( OutlinedTextField(
modifier = Modifier.fillMaxWidth().clickable { showAuthPrompt = true }, modifier = Modifier
.fillMaxWidth()
.clickable { showAuthPrompt = true },
value = uiState.interfaceProxy.privateKey, value = uiState.interfaceProxy.privateKey,
visualTransformation = visualTransformation =
if ((id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated) if ((id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated)
@@ -392,7 +416,9 @@ fun ConfigScreen(
) )
OutlinedTextField( OutlinedTextField(
modifier = modifier =
Modifier.fillMaxWidth().focusRequester(FocusRequester.Default), Modifier
.fillMaxWidth()
.focusRequester(FocusRequester.Default),
value = uiState.interfaceProxy.publicKey, value = uiState.interfaceProxy.publicKey,
enabled = false, enabled = false,
onValueChange = {}, onValueChange = {},
@@ -425,7 +451,9 @@ fun ConfigScreen(
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
label = stringResource(R.string.addresses), label = stringResource(R.string.addresses),
hint = stringResource(R.string.comma_separated_list), hint = stringResource(R.string.comma_separated_list),
modifier = Modifier.fillMaxWidth(3 / 5f).padding(end = 5.dp), modifier = Modifier
.fillMaxWidth(3 / 5f)
.padding(end = 5.dp),
) )
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.interfaceProxy.listenPort, value = uiState.interfaceProxy.listenPort,
@@ -443,7 +471,9 @@ fun ConfigScreen(
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
label = stringResource(R.string.dns_servers), label = stringResource(R.string.dns_servers),
hint = stringResource(R.string.comma_separated_list), hint = stringResource(R.string.comma_separated_list),
modifier = Modifier.fillMaxWidth(3 / 5f).padding(end = 5.dp), modifier = Modifier
.fillMaxWidth(3 / 5f)
.padding(end = 5.dp),
) )
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.interfaceProxy.mtu, value = uiState.interfaceProxy.mtu,
@@ -456,11 +486,13 @@ fun ConfigScreen(
} }
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxSize().padding(top = 5.dp), modifier = Modifier
.fillMaxSize()
.padding(top = 5.dp),
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
) { ) {
TextButton(onClick = { showApplicationsDialog = true }) { TextButton(onClick = { showApplicationsDialog = true }) {
Text(applicationButtonText()) Text(applicationButtonText.text)
} }
} }
} }
@@ -473,7 +505,9 @@ fun ConfigScreen(
color = MaterialTheme.colorScheme.surface, color = MaterialTheme.colorScheme.surface,
modifier = modifier =
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { (if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Modifier.fillMaxHeight(fillMaxHeight).fillMaxWidth(fillMaxWidth) Modifier
.fillMaxHeight(fillMaxHeight)
.fillMaxWidth(fillMaxWidth)
} else { } else {
Modifier.fillMaxWidth(fillMaxWidth) Modifier.fillMaxWidth(fillMaxWidth)
}) })
@@ -482,12 +516,16 @@ fun ConfigScreen(
Column( Column(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
modifier = Modifier.padding(horizontal = 15.dp).padding(bottom = 10.dp), modifier = Modifier
.padding(horizontal = 15.dp)
.padding(bottom = 10.dp),
) { ) {
Row( Row(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().padding(horizontal = 5.dp), modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 5.dp),
) { ) {
SectionTitle( SectionTitle(
stringResource(R.string.peer), stringResource(R.string.peer),
@@ -570,7 +608,9 @@ fun ConfigScreen(
Row( Row(
horizontalArrangement = Arrangement.SpaceEvenly, horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxSize().padding(bottom = 140.dp), modifier = Modifier
.fillMaxSize()
.padding(bottom = 140.dp),
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@@ -1,25 +1,25 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main package com.zaneschepke.wireguardautotunnel.ui.screens.main
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable import androidx.compose.foundation.focusable
import androidx.compose.foundation.gestures.ScrollableDefaults import androidx.compose.foundation.gestures.ScrollableDefaults
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@@ -44,10 +44,10 @@ import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material.icons.rounded.Info import androidx.compose.material.icons.rounded.Info
import androidx.compose.material.icons.rounded.Star import androidx.compose.material.icons.rounded.Star
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FabPosition import androidx.compose.material3.FabPosition
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -81,6 +81,7 @@ import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
@@ -88,6 +89,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController import androidx.navigation.NavController
import com.journeyapps.barcodescanner.ScanContract import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions import com.journeyapps.barcodescanner.ScanOptions
import com.wireguard.android.backend.GoBackend
import com.wireguard.android.backend.Tunnel import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
@@ -108,12 +110,10 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable @Composable
fun MainScreen( fun MainScreen(
viewModel: MainViewModel = hiltViewModel(), viewModel: MainViewModel = hiltViewModel(),
padding: PaddingValues,
focusRequester: FocusRequester, focusRequester: FocusRequester,
showSnackbarMessage: (String) -> Unit, showSnackbarMessage: (String) -> Unit,
navController: NavController navController: NavController
@@ -131,6 +131,17 @@ fun MainScreen(
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) } var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
val uiState by viewModel.uiState.collectAsStateWithLifecycle() val uiState by viewModel.uiState.collectAsStateWithLifecycle()
var vpnIntent by remember { mutableStateOf(GoBackend.VpnService.prepare(WireGuardAutoTunnel.instance)) }
val vpnActivityResultState =
rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult(),
onResult = {
val accepted = (it.resultCode == AppCompatActivity.RESULT_OK)
if (accepted) {
vpnIntent = null
}
},
)
LaunchedEffect(uiState.loading) { LaunchedEffect(uiState.loading) {
if (!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) { if (!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) {
delay(Constants.FOCUS_REQUEST_DELAY) delay(Constants.FOCUS_REQUEST_DELAY)
@@ -254,6 +265,9 @@ fun MainScreen(
} }
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) { fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
if (vpnIntent != null) {
return vpnActivityResultState.launch(vpnIntent)
}
if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop() if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop()
} }
@@ -275,8 +289,9 @@ fun MainScreen(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = modifier =
Modifier.requiredWidth(LocalConfiguration.current.screenWidthDp.dp) Modifier
.padding(end = 5.dp), .requiredWidth(LocalConfiguration.current.screenWidthDp.dp)
.padding(end = 5.dp)
) { ) {
Row { Row {
Icon( Icon(
@@ -287,8 +302,15 @@ fun MainScreen(
if (uiState.settings.isAutoTunnelPaused) Color.Gray if (uiState.settings.isAutoTunnelPaused) Color.Gray
else mint, else mint,
) )
val autoTunnelingLabel = buildAnnotatedString {
append(stringResource(id = R.string.auto_tunneling))
append(": ")
if(uiState.settings.isAutoTunnelPaused) append(stringResource(id = R.string.paused)) else append(
stringResource(id = R.string.active),
)
}
Text( Text(
"Auto-tunneling: ${if (uiState.settings.isAutoTunnelPaused) "paused" else "active"}", autoTunnelingLabel.text,
style = typography.bodyLarge, style = typography.bodyLarge,
modifier = Modifier.padding(start = 10.dp), modifier = Modifier.padding(start = 10.dp),
) )
@@ -298,14 +320,14 @@ fun MainScreen(
onClick = { viewModel.resumeAutoTunneling() }, onClick = { viewModel.resumeAutoTunneling() },
modifier = Modifier.padding(end = 10.dp), modifier = Modifier.padding(end = 10.dp),
) { ) {
Text("Resume") Text(stringResource(id = R.string.resume))
} }
else else
TextButton( TextButton(
onClick = { viewModel.pauseAutoTunneling() }, onClick = { viewModel.pauseAutoTunneling() },
modifier = Modifier.padding(end = 10.dp), modifier = Modifier.padding(end = 10.dp),
) { ) {
Text("Pause") Text(stringResource(id = R.string.pause))
} }
} }
}, },
@@ -322,18 +344,18 @@ fun MainScreen(
var fobColor by remember { mutableStateOf(secondaryColor) } var fobColor by remember { mutableStateOf(secondaryColor) }
FloatingActionButton( FloatingActionButton(
modifier = modifier =
(if ( (if (
WireGuardAutoTunnel.isRunningOnAndroidTv() && WireGuardAutoTunnel.isRunningOnAndroidTv() &&
uiState.tunnels.isEmpty() uiState.tunnels.isEmpty()
) )
Modifier.focusRequester(focusRequester) Modifier.focusRequester(focusRequester)
else Modifier) else Modifier)
.padding(bottom = 90.dp) .padding(bottom = 90.dp)
.onFocusChanged { .onFocusChanged {
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
fobColor = if (it.isFocused) hoverColor else secondaryColor fobColor = if (it.isFocused) hoverColor else secondaryColor
} }
}, },
onClick = { showBottomSheet = true }, onClick = { showBottomSheet = true },
containerColor = fobColor, containerColor = fobColor,
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
@@ -351,7 +373,9 @@ fun MainScreen(
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxSize().padding(padding), modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
) { ) {
Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic) Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic)
} }
@@ -364,12 +388,13 @@ fun MainScreen(
// Sheet content // Sheet content
Row( Row(
modifier = modifier =
Modifier.fillMaxWidth() Modifier
.clickable { .fillMaxWidth()
showBottomSheet = false .clickable {
tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES) showBottomSheet = false
} tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES)
.padding(10.dp), }
.padding(10.dp),
) { ) {
Icon( Icon(
Icons.Filled.FileOpen, Icons.Filled.FileOpen,
@@ -382,26 +407,27 @@ fun MainScreen(
) )
} }
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) { if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Divider() HorizontalDivider()
Row( Row(
modifier = modifier =
Modifier.fillMaxWidth() Modifier
.clickable { .fillMaxWidth()
scope.launch { .clickable {
showBottomSheet = false scope.launch {
val scanOptions = ScanOptions() showBottomSheet = false
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE) val scanOptions = ScanOptions()
scanOptions.setOrientationLocked(true) scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
scanOptions.setPrompt( scanOptions.setOrientationLocked(true)
context.getString(R.string.scanning_qr) scanOptions.setPrompt(
) context.getString(R.string.scanning_qr)
scanOptions.setBeepEnabled(false) )
scanOptions.captureActivity = scanOptions.setBeepEnabled(false)
CaptureActivityPortrait::class.java scanOptions.captureActivity =
scanLauncher.launch(scanOptions) CaptureActivityPortrait::class.java
} scanLauncher.launch(scanOptions)
} }
.padding(10.dp), }
.padding(10.dp),
) { ) {
Icon( Icon(
Icons.Filled.QrCode, Icons.Filled.QrCode,
@@ -414,17 +440,18 @@ fun MainScreen(
) )
} }
} }
Divider() HorizontalDivider()
Row( Row(
modifier = modifier =
Modifier.fillMaxWidth() Modifier
.clickable { .fillMaxWidth()
showBottomSheet = false .clickable {
navController.navigate( showBottomSheet = false
"${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}", navController.navigate(
) "${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}",
} )
.padding(10.dp), }
.padding(10.dp),
) { ) {
Icon( Icon(
Icons.Filled.Create, Icons.Filled.Create,
@@ -443,10 +470,11 @@ fun MainScreen(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
modifier = modifier =
Modifier.fillMaxWidth() Modifier
.fillMaxHeight(.90f) .fillMaxWidth()
.overscroll(ScrollableDefaults.overscrollEffect()) .fillMaxHeight(.90f)
.padding(innerPadding), .overscroll(ScrollableDefaults.overscrollEffect())
.padding(innerPadding),
state = rememberLazyListState(0, uiState.tunnels.count()), state = rememberLazyListState(0, uiState.tunnels.count()),
userScrollEnabled = true, userScrollEnabled = true,
reverseLayout = true, reverseLayout = true,
@@ -486,14 +514,18 @@ fun MainScreen(
Icons.Rounded.Star, Icons.Rounded.Star,
stringResource(R.string.status), stringResource(R.string.status),
tint = leadingIconColor, tint = leadingIconColor,
modifier = Modifier.padding(end = 10.dp).size(20.dp), modifier = Modifier
.padding(end = 10.dp)
.size(20.dp),
) )
} else { } else {
Icon( Icon(
Icons.Rounded.Circle, Icons.Rounded.Circle,
stringResource(R.string.status), stringResource(R.string.status),
tint = leadingIconColor, tint = leadingIconColor,
modifier = Modifier.padding(end = 15.dp).size(15.dp), modifier = Modifier
.padding(end = 15.dp)
.size(15.dp),
) )
} }
}, },
@@ -94,8 +94,8 @@ import java.io.File
) )
@Composable @Composable
fun SettingsScreen( fun SettingsScreen(
viewModel: SettingsViewModel = hiltViewModel(),
padding: PaddingValues, padding: PaddingValues,
viewModel: SettingsViewModel = hiltViewModel(),
showSnackbarMessage: (String) -> Unit, showSnackbarMessage: (String) -> Unit,
focusRequester: FocusRequester focusRequester: FocusRequester
) { ) {
@@ -127,7 +127,7 @@ fun SettingsScreen(
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
result: ActivityResult -> result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK) { if (result.resultCode == Activity.RESULT_OK) {
val intent = result.data result.data
// Handle the Intent // Handle the Intent
} }
viewModel.setBatteryOptimizeDisableShown() viewModel.setBatteryOptimizeDisableShown()
@@ -329,7 +329,7 @@ fun SettingsScreen(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
modifier = modifier =
Modifier.fillMaxSize().verticalScroll(scrollState).clickable( Modifier.fillMaxSize().padding(padding).verticalScroll(scrollState).clickable(
indication = null, indication = null,
interactionSource = interactionSource, interactionSource = interactionSource,
) { ) {
@@ -347,7 +347,7 @@ fun SettingsScreen(
.fillMaxWidth(fillMaxWidth) .fillMaxWidth(fillMaxWidth)
.padding(top = 10.dp) .padding(top = 10.dp)
} else { } else {
Modifier.fillMaxWidth(fillMaxWidth).padding(top = 60.dp) Modifier.fillMaxWidth(fillMaxWidth).padding(top = 20.dp)
}) })
.padding(bottom = 10.dp), .padding(bottom = 10.dp),
) { ) {
@@ -20,10 +20,10 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ArrowForward import androidx.compose.material.icons.automirrored.rounded.ArrowForward
import androidx.compose.material.icons.rounded.Book import androidx.compose.material.icons.rounded.Book
import androidx.compose.material.icons.rounded.Mail import androidx.compose.material.icons.rounded.Mail
import androidx.compose.material3.Divider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
@@ -57,8 +57,8 @@ import com.zaneschepke.wireguardautotunnel.util.Event
@Composable @Composable
fun SupportScreen( fun SupportScreen(
viewModel: SupportViewModel = hiltViewModel(),
padding: PaddingValues, padding: PaddingValues,
viewModel: SupportViewModel = hiltViewModel(),
showSnackbarMessage: (String) -> Unit, showSnackbarMessage: (String) -> Unit,
focusRequester: FocusRequester focusRequester: FocusRequester
) { ) {
@@ -104,10 +104,9 @@ fun SupportScreen(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
modifier = modifier =
Modifier.fillMaxSize() Modifier.fillMaxSize().padding(padding)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.focusable() .focusable()
.padding(padding),
) { ) {
Surface( Surface(
tonalElevation = 2.dp, tonalElevation = 2.dp,
@@ -155,10 +154,16 @@ fun SupportScreen(
modifier = Modifier.padding(start = 10.dp), modifier = Modifier.padding(start = 10.dp),
) )
} }
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go)) Icon(
Icons.AutoMirrored.Rounded.ArrowForward,
stringResource(id = R.string.go)
)
} }
} }
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp) HorizontalDivider(
thickness = 0.5.dp,
color = MaterialTheme.colorScheme.onBackground
)
TextButton( TextButton(
onClick = { openWebPage(context.resources.getString(R.string.discord_url)) }, onClick = { openWebPage(context.resources.getString(R.string.discord_url)) },
modifier = Modifier.padding(vertical = 5.dp), modifier = Modifier.padding(vertical = 5.dp),
@@ -180,10 +185,16 @@ fun SupportScreen(
modifier = Modifier.padding(start = 10.dp), modifier = Modifier.padding(start = 10.dp),
) )
} }
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go)) Icon(
Icons.AutoMirrored.Rounded.ArrowForward,
stringResource(id = R.string.go)
)
} }
} }
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp) HorizontalDivider(
thickness = 0.5.dp,
color = MaterialTheme.colorScheme.onBackground
)
TextButton( TextButton(
onClick = { openWebPage(context.resources.getString(R.string.github_url)) }, onClick = { openWebPage(context.resources.getString(R.string.github_url)) },
modifier = Modifier.padding(vertical = 5.dp), modifier = Modifier.padding(vertical = 5.dp),
@@ -205,10 +216,16 @@ fun SupportScreen(
modifier = Modifier.padding(start = 10.dp), modifier = Modifier.padding(start = 10.dp),
) )
} }
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go)) Icon(
Icons.AutoMirrored.Rounded.ArrowForward,
stringResource(id = R.string.go)
)
} }
} }
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp) HorizontalDivider(
thickness = 0.5.dp,
color = MaterialTheme.colorScheme.onBackground
)
TextButton( TextButton(
onClick = { launchEmail() }, onClick = { launchEmail() },
modifier = Modifier.padding(vertical = 5.dp), modifier = Modifier.padding(vertical = 5.dp),
@@ -226,7 +243,10 @@ fun SupportScreen(
modifier = Modifier.padding(start = 10.dp), modifier = Modifier.padding(start = 10.dp),
) )
} }
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go)) Icon(
Icons.AutoMirrored.Rounded.ArrowForward,
stringResource(id = R.string.go)
)
} }
} }
} }
@@ -1,22 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.graphics.Color
import com.google.accompanist.systemuicontroller.rememberSystemUiController
@Composable
fun TransparentSystemBars() {
val systemUiController = rememberSystemUiController()
val useDarkIcons = !isSystemInDarkTheme()
DisposableEffect(systemUiController, useDarkIcons) {
systemUiController.setSystemBarsColor(
color = Color.Transparent,
darkIcons = useDarkIcons,
)
onDispose {}
}
}
+12 -3
View File
@@ -73,7 +73,7 @@
<string name="last_handshake">Last handshake</string> <string name="last_handshake">Last handshake</string>
<string name="name">Name</string> <string name="name">Name</string>
<string name="restart">Restart Tunnel</string> <string name="restart">Restart Tunnel</string>
<string name="vpn_connection_failed">VPN Connection Failed</string> <string name="vpn_connection_failed">Connection failed</string>
<string name="failed_connection_to">Failed connection to -</string> <string name="failed_connection_to">Failed connection to -</string>
<string name="initial_connection_failure_message">Attempting to connect to server after 30 seconds of no response.</string> <string name="initial_connection_failure_message">Attempting to connect to server after 30 seconds of no response.</string>
<string name="lost_connection_failure_message">Attempting to reconnect to server after more than one minute of no response.</string> <string name="lost_connection_failure_message">Attempting to reconnect to server after more than one minute of no response.</string>
@@ -90,7 +90,7 @@
<string name="clear_icon">Clear Icon</string> <string name="clear_icon">Clear Icon</string>
<string name="search_icon">Search Icon</string> <string name="search_icon">Search Icon</string>
<string name="attempt_connection">Attempting connection..</string> <string name="attempt_connection">Attempting connection..</string>
<string name="vpn_starting">VPN Starting</string> <string name="vpn_starting">VPN starting</string>
<string name="db_name">wg-tunnel-db</string> <string name="db_name">wg-tunnel-db</string>
<string name="scanning_qr">Scanning for QR</string> <string name="scanning_qr">Scanning for QR</string>
<string name="qr_result_failed">QR scan failed</string> <string name="qr_result_failed">QR scan failed</string>
@@ -140,7 +140,7 @@
<string name="exported_configs_message">Exported configs to downloads</string> <string name="exported_configs_message">Exported configs to downloads</string>
<string name="status">status</string> <string name="status">status</string>
<string name="tunnel_on_wifi">Tunnel on untrusted wifi</string> <string name="tunnel_on_wifi">Tunnel on untrusted wifi</string>
<string name="my_email" translatable="false">zanecschepke@gmail.com</string> <string name="my_email" translatable="false">support@zaneschepke.com</string>
<string name="email_subject">WG Tunnel Support</string> <string name="email_subject">WG Tunnel Support</string>
<string name="email_chooser">Send an email…</string> <string name="email_chooser">Send an email…</string>
<string name="go">go</string> <string name="go">go</string>
@@ -167,4 +167,13 @@
<string name="delete_tunnel">Delete tunnel</string> <string name="delete_tunnel">Delete tunnel</string>
<string name="delete_tunnel_message">Are you sure you would like to delete this tunnel?</string> <string name="delete_tunnel_message">Are you sure you would like to delete this tunnel?</string>
<string name="yes">Yes</string> <string name="yes">Yes</string>
<string name="resume">Resume</string>
<string name="pause">Pause</string>
<string name="paused">paused</string>
<string name="active">active</string>
<string name="tunneling_apps">Tunneling apps</string>
<string name="included">included</string>
<string name="excluded">excluded</string>
<string name="all">all</string>
<string name="always_on_disabled">Always-on VPN attempted to start a tunnel, but this feature is disabled in settings.</string>
</resources> </resources>
+12
View File
@@ -1,4 +1,5 @@
import org.gradle.api.invocation.Gradle import org.gradle.api.invocation.Gradle
import java.io.File
object BuildHelper { object BuildHelper {
private fun getCurrentFlavor(gradle: Gradle): String { private fun getCurrentFlavor(gradle: Gradle): String {
@@ -21,6 +22,17 @@ object BuildHelper {
return flavor return flavor
} }
fun getLocalProperty(key: String, file: String = "local.properties"): String? {
val properties = java.util.Properties()
val localProperties = File(file)
if (localProperties.isFile) {
java.io.InputStreamReader(java.io.FileInputStream(localProperties), Charsets.UTF_8).use { reader ->
properties.load(reader)
}
} else return null
return properties.getProperty(key)
}
fun isGeneralFlavor(gradle: Gradle): Boolean { fun isGeneralFlavor(gradle: Gradle): Boolean {
return getCurrentFlavor(gradle) == "general" return getCurrentFlavor(gradle) == "general"
} }
+2 -2
View File
@@ -1,7 +1,7 @@
object Constants { object Constants {
const val VERSION_NAME = "3.3.5" const val VERSION_NAME = "3.3.8"
const val JVM_TARGET = "17" const val JVM_TARGET = "17"
const val VERSION_CODE = 33500 const val VERSION_CODE = 33800
const val TARGET_SDK = 34 const val TARGET_SDK = 34
const val MIN_SDK = 26 const val MIN_SDK = 26
const val APP_ID = "com.zaneschepke.wireguardautotunnel" const val APP_ID = "com.zaneschepke.wireguardautotunnel"
@@ -0,0 +1,5 @@
What's new:
- Improve first launch flow
- Switch to wireguard lib fork
- Request VPN permission on first VPN start
- Bump versions
@@ -0,0 +1,2 @@
What's new:
- Tunnel display UI bug fix
@@ -0,0 +1,4 @@
What's new:
- Config edit UI bug fix
- Add GrapheneOS first launch AOVPN notification
- Bump versions
+14 -15
View File
@@ -10,35 +10,35 @@ datastorePreferences = "1.0.0"
desugar_jdk_libs = "2.0.4" desugar_jdk_libs = "2.0.4"
espressoCore = "3.5.1" espressoCore = "3.5.1"
firebase-crashlytics-gradle = "2.9.9" firebase-crashlytics-gradle = "2.9.9"
google-services = "4.4.0" google-services = "4.4.1"
hiltAndroid = "2.50" hiltAndroid = "2.50"
hiltNavigationCompose = "1.1.0" hiltNavigationCompose = "1.1.0"
junit = "4.13.2" junit = "4.13.2"
kotlinx-serialization-json = "1.6.2" kotlinx-serialization-json = "1.6.3"
lifecycle-runtime-compose = "2.7.0" lifecycle-runtime-compose = "2.7.0"
material3 = "1.2.0-rc01" material3 = "1.2.0"
navigationCompose = "2.7.6" navigationCompose = "2.7.7"
roomVersion = "2.6.1" roomVersion = "2.6.1"
timber = "5.0.1" timber = "5.0.1"
tunnel = "1.0.20230706" tunnel = "1.1.0"
androidGradlePlugin = "8.2.2" androidGradlePlugin = "8.3.0-rc02"
kotlin = "1.9.22" kotlin = "1.9.22"
ksp = "1.9.22-1.0.16" ksp = "1.9.22-1.0.16"
composeBom = "2024.01.00" composeBom = "2024.02.00"
firebaseBom = "32.7.1" firebaseBom = "32.7.2"
compose = "1.6.0" compose = "1.6.1"
crashlytics = "18.6.1" crashlytics = "18.6.2"
analytics = "21.5.0" analytics = "21.5.1"
zxingAndroidEmbedded = "4.3.0" zxingAndroidEmbedded = "4.3.0"
zxingCore = "3.5.2" zxingCore = "3.5.3"
[libraries] [libraries]
# accompanist # accompanist
accompanist-drawablepainter = { module = "com.google.accompanist:accompanist-drawablepainter", version.ref = "accompanist" } accompanist-drawablepainter = { module = "com.google.accompanist:accompanist-drawablepainter", version.ref = "accompanist" }
accompanist-flowlayout = { module = "com.google.accompanist:accompanist-flowlayout", version.ref = "accompanist" } accompanist-flowlayout = { module = "com.google.accompanist:accompanist-flowlayout", version.ref = "accompanist" }
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" } accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" }
#room #room
androidx-biometric-ktx = { module = "androidx.biometric:biometric-ktx", version.ref = "biometricKtx" } androidx-biometric-ktx = { module = "androidx.biometric:biometric-ktx", version.ref = "biometricKtx" }
@@ -82,7 +82,7 @@ lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-com
material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "compose" } material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "compose" }
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
tunnel = { module = "com.wireguard.android:tunnel", version.ref = "tunnel" } tunnel = { module = "com.zaneschepke:wireguard-android", version.ref = "tunnel" }
#firebase #firebase
google-firebase-crashlytics-ktx = { module = "com.google.firebase:firebase-crashlytics-ktx", version.ref = "crashlytics" } google-firebase-crashlytics-ktx = { module = "com.google.firebase:firebase-crashlytics-ktx", version.ref = "crashlytics" }
@@ -93,7 +93,6 @@ google-services = { module = "com.google.gms:google-services", version.ref = "go
zxing-core = { module = "com.google.zxing:core", version.ref = "zxingCore" } zxing-core = { module = "com.google.zxing:core", version.ref = "zxingCore" }
zxing-android-embedded = { module = "com.journeyapps:zxing-android-embedded", version.ref = "zxingAndroidEmbedded" } zxing-android-embedded = { module = "com.journeyapps:zxing-android-embedded", version.ref = "zxingAndroidEmbedded" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+2
View File
@@ -2,5 +2,7 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
distributionSha256Sum=3e1af3ae886920c3ac87f7a91f816c0c7c436f276a6eefdb3da152100fef72ae
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
+24
View File
@@ -2,18 +2,42 @@ pluginManagement {
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
mavenLocal()
gradlePluginPortal() gradlePluginPortal()
} }
} }
val GITHUB_USER_VAR = "GH_USER"
val GITHUB_TOKEN_VAR = "GH_TOKEN"
dependencyResolutionManagement { dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories { repositories {
maven {
name = "GitHubPackages"
url = uri("https://maven.pkg.github.com/zaneschepke/wireguard-android")
credentials {
username = getLocalProperty(GITHUB_USER_VAR) ?: System.getenv(GITHUB_USER_VAR)
password = getLocalProperty(GITHUB_TOKEN_VAR) ?: System.getenv(GITHUB_TOKEN_VAR)
}
}
google() google()
mavenCentral() mavenCentral()
mavenLocal()
} }
} }
fun getLocalProperty(key: String, file: String = "local.properties"): String? {
val properties = java.util.Properties()
val localProperties = File(file)
if (localProperties.isFile) {
java.io.InputStreamReader(java.io.FileInputStream(localProperties), Charsets.UTF_8).use { reader ->
properties.load(reader)
}
} else return null
return properties.getProperty(key)
}
rootProject.name = "WG Tunnel" rootProject.name = "WG Tunnel"
include(":app") include(":app")