diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml
index 002020ad..ee9ca314 100644
--- a/.github/workflows/check.yaml
+++ b/.github/workflows/check.yaml
@@ -1,6 +1,6 @@
name: Run tests
-on: [ push, pull_request ]
+on: [ pull_request ]
jobs:
build:
@@ -17,9 +17,9 @@ jobs:
-x :ics-openvpn-main:buildCMakeRelWithDebInfo[x86]
-x :ics-openvpn-main:configureCMakeRelWithDebInfo[x86_64]
-x :ics-openvpn-main:buildCMakeRelWithDebInfo[x86_64]
- -x :ics-openvpn-main:externalNativeBuildUiRelease
- -x :ics-openvpn-main:packageUiReleaseResources
- -x :ics-openvpn-main:compileUiReleaseJavaWithJavac
+ -x :ics-openvpn-main:externalNativeBuildUiOvpn23Release
+ -x :ics-openvpn-main:packageUiOvpn23ReleaseResources
+ -x :ics-openvpn-main:compileUiOvpn23ReleaseJavaWithJavac
-x :ics-openvpn-main:configureCMakeDebug[x86_64]
-x :ics-openvpn-main:buildCMakeDebug[x86_64]
-x :ics-openvpn-main:buildCMakeDebug[arm64-v8a]
@@ -29,14 +29,16 @@ jobs:
matrix:
api-level: [ 31 ]
steps:
- - name: Use Java 17
- run: echo "JAVA_HOME=${!JAVA_HOME_17_X64}" >> $GITHUB_ENV
-
- name: Checkout repository and submodules
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
with:
submodules: recursive
-
+ - name: Install Java 17
+ uses: actions/setup-java@v3
+ with:
+ distribution: 'temurin'
+ java-version: '17'
+ cache: 'gradle'
# Based on https://github.com/actions/cache/blob/main/examples.md#java---gradle
- name: Cache Gradle caches and wrapper
uses: actions/cache@v3
diff --git a/app/build.gradle b/app/build.gradle
index 77ca38b0..aa710135 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -9,15 +9,15 @@ plugins {
}
android {
- compileSdk 33
+ compileSdk 34
defaultConfig {
applicationId "nl.eduvpn.app"
minSdkVersion 21
- targetSdkVersion 31
+ targetSdkVersion 34
versionCode 23
versionName "3.1.1"
- ndkVersion "21.0.6113669"
+ ndkVersion "26.1.10909125"
vectorDrawables.useSupportLibrary = true
@@ -34,12 +34,12 @@ android {
buildConfigField "String", "OAUTH_CLIENT_ID", "\"org.eduvpn.app.android\""
buildConfigField "String", "OAUTH_REDIRECT_URI", "\"org.eduvpn.app:/api/callback\""
buildConfigField "String", "OAUTH_SCOPE", "\"config\""
- buildConfigField "String", "CERTIFICATE_DISPLAY_NAME", "\"eduVPN for Android\""
manifestPlaceholders = [
'redirectScheme': 'org.eduvpn.app'
]
- missingDimensionStrategy 'implementation', 'ui' // Skeleton is no option for us because we need the log activity
+ missingDimensionStrategy 'implementation', 'ui' // Skeleton is no option for us because we need the log activity
+ missingDimensionStrategy 'ovpnimpl', 'ovpn23'
// This excludes some pretty old ABIs such armeabi or mips. LibSodium-JNI still includes binaries for these, which
// leads to some devices selecting these as the app ABI, but the OpenVPN library did not include a binary for these,
@@ -77,7 +77,7 @@ android {
}
}
- flavorDimensions "product"
+ flavorDimensions = ["product"]
productFlavors {
basic {
@@ -88,6 +88,18 @@ android {
dimension "product"
applicationId "nl.eduvpn.app.dev"
}
+ gov {
+ dimension "product"
+ applicationId "org.govvpn.app"
+ // API discovery is disabled, and only custom URLs can be entered.
+ buildConfigField "boolean", "API_DISCOVERY_ENABLED", "false"
+ buildConfigField "String", "OAUTH_CLIENT_ID", "\"org.govvpn.app.android\""
+ buildConfigField "String", "OAUTH_REDIRECT_URI", "\"org.govvpn.app:/api/callback\""
+
+ manifestPlaceholders = [
+ 'redirectScheme': 'org.govvpn.app.android'
+ ]
+ }
home {
dimension "product"
applicationId "org.letsconnect_vpn.app"
@@ -95,7 +107,6 @@ android {
buildConfigField "boolean", "API_DISCOVERY_ENABLED", "false"
buildConfigField "String", "OAUTH_CLIENT_ID", "\"org.letsconnect-vpn.app.android\""
buildConfigField "String", "OAUTH_REDIRECT_URI", "\"org.letsconnect-vpn.app:/api/callback\""
- buildConfigField "String", "CERTIFICATE_DISPLAY_NAME", "\"Let's Connect! for Android\""
manifestPlaceholders = [
'redirectScheme': 'org.letsconnect-vpn.app'
@@ -103,23 +114,17 @@ android {
}
}
- lintOptions {
- disable 'GradleDependency', // Gradle dependencies can be a bit outdated, since we prefer to use the same versions as in the VPN library
- 'UnsafeNativeCodeLocation', // The OpenVPN .so files are put to a different place, as per the documentation
- 'RtlSymmetry', 'RtlHardcoded', // No support for RTL as of now
- 'MissingTranslation' // The OpenVPN library contains translations for a lot off languages. This app only has english, so it complains that we are missing translations for the other languages
- }
compileOptions {
// Support Java 8+ on sdk < 24, also necessary for WireGuard
coreLibraryDesugaringEnabled true
- sourceCompatibility JavaVersion.VERSION_11
- targetCompatibility JavaVersion.VERSION_11
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
- jvmTarget = JavaVersion.VERSION_11
+ jvmTarget = JavaVersion.VERSION_17
}
kapt {
@@ -132,22 +137,26 @@ android {
option("-Werror")
}
}
-
packagingOptions {
- // Created by kotlinx-coroutines-core for debugging
- exclude "DebugProbesKt.bin"
+ resources {
+ excludes += ['DebugProbesKt.bin']
+ }
+ }
+
+ namespace 'nl.eduvpn.app'
+ lint {
+ disable 'GradleDependency', 'UnsafeNativeCodeLocation', 'RtlSymmetry', 'RtlHardcoded', 'MissingTranslation'
}
}
-def daggerVersion = "2.44"
-def okHttpVersion = "4.10.0"
+def daggerVersion = "2.48"
def lifecycleVersion = "2.2.0"
dependencies {
// OpenVPN library
implementation project(path: ':ics-openvpn-main')
// WireGuard VPN library
- implementation 'com.wireguard.android:tunnel:1.0.20230427'
+ implementation 'com.wireguard.android:tunnel:1.0.20230706'
// eduVPN common library written in Go, stores all data and does the communication with the servers
implementation project(path: ':common')
// Please try to stay in sync with the versions used in the ics-openvpn module
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 36173d70..e28a6787 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -1,17 +1,5 @@
-# Add project specific ProGuard rules here.
-# By default, the flags in this file are appended to flags specified
-# in /home/fkooman/Android/Sdk/tools/proguard/proguard-android.txt
-# You can edit the include path and order by changing the proguardFiles
-# directive in build.gradle.
-#
-# For more details, see
-# http://developer.android.com/guide/developing/tools/proguard.html
-
-# Add any project specific keep options here:
-
-# If your project uses WebView with JS, uncomment the following
-# and specify the fully qualified class name to the JavaScript interface
-# class:
-#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
-# public *;
-#}
+-dontwarn com.google.errorprone.annotations.*
+-dontwarn org.bouncycastle.jsse.**
+-dontwarn org.conscrypt.*
+-dontwarn org.openjsse.javax.net.ssl.*
+-dontwarn org.openjsse.net.ssl.*
diff --git a/app/src/gov/res/drawable-hdpi/logo_black.png b/app/src/gov/res/drawable-hdpi/logo_black.png
new file mode 100644
index 00000000..8cd1cdaf
Binary files /dev/null and b/app/src/gov/res/drawable-hdpi/logo_black.png differ
diff --git a/app/src/gov/res/drawable-mdpi/logo_black.png b/app/src/gov/res/drawable-mdpi/logo_black.png
new file mode 100644
index 00000000..e54c4a5c
Binary files /dev/null and b/app/src/gov/res/drawable-mdpi/logo_black.png differ
diff --git a/app/src/gov/res/drawable-xhdpi/logo_black.png b/app/src/gov/res/drawable-xhdpi/logo_black.png
new file mode 100644
index 00000000..240dd822
Binary files /dev/null and b/app/src/gov/res/drawable-xhdpi/logo_black.png differ
diff --git a/app/src/gov/res/drawable-xxhdpi/logo_black.png b/app/src/gov/res/drawable-xxhdpi/logo_black.png
new file mode 100644
index 00000000..3acc6e80
Binary files /dev/null and b/app/src/gov/res/drawable-xxhdpi/logo_black.png differ
diff --git a/app/src/gov/res/drawable-xxxhdpi/logo_black.png b/app/src/gov/res/drawable-xxxhdpi/logo_black.png
new file mode 100644
index 00000000..ed2651b9
Binary files /dev/null and b/app/src/gov/res/drawable-xxxhdpi/logo_black.png differ
diff --git a/app/src/gov/res/drawable/ic_application_logo.xml b/app/src/gov/res/drawable/ic_application_logo.xml
new file mode 100644
index 00000000..eb028ce9
--- /dev/null
+++ b/app/src/gov/res/drawable/ic_application_logo.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/gov/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/gov/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 00000000..036d09bc
--- /dev/null
+++ b/app/src/gov/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/gov/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/gov/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 00000000..036d09bc
--- /dev/null
+++ b/app/src/gov/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/gov/res/mipmap-hdpi/ic_launcher.webp b/app/src/gov/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 00000000..901ebb46
Binary files /dev/null and b/app/src/gov/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/app/src/gov/res/mipmap-hdpi/ic_launcher_foreground.webp b/app/src/gov/res/mipmap-hdpi/ic_launcher_foreground.webp
new file mode 100644
index 00000000..11ffd5ed
Binary files /dev/null and b/app/src/gov/res/mipmap-hdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/gov/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/gov/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..933a9299
Binary files /dev/null and b/app/src/gov/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/app/src/gov/res/mipmap-mdpi/ic_launcher.webp b/app/src/gov/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 00000000..04f58078
Binary files /dev/null and b/app/src/gov/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/app/src/gov/res/mipmap-mdpi/ic_launcher_foreground.webp b/app/src/gov/res/mipmap-mdpi/ic_launcher_foreground.webp
new file mode 100644
index 00000000..48e5ea20
Binary files /dev/null and b/app/src/gov/res/mipmap-mdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/gov/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/gov/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..e5cdc73f
Binary files /dev/null and b/app/src/gov/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/app/src/gov/res/mipmap-xhdpi/ic_launcher.webp b/app/src/gov/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 00000000..8020e4f5
Binary files /dev/null and b/app/src/gov/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/app/src/gov/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app/src/gov/res/mipmap-xhdpi/ic_launcher_foreground.webp
new file mode 100644
index 00000000..7c8d65a7
Binary files /dev/null and b/app/src/gov/res/mipmap-xhdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/gov/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/gov/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..957ae6b0
Binary files /dev/null and b/app/src/gov/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/app/src/gov/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/gov/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 00000000..91bc0c96
Binary files /dev/null and b/app/src/gov/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/app/src/gov/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/app/src/gov/res/mipmap-xxhdpi/ic_launcher_foreground.webp
new file mode 100644
index 00000000..625be23a
Binary files /dev/null and b/app/src/gov/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/gov/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/gov/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..80b4e503
Binary files /dev/null and b/app/src/gov/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/gov/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/gov/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 00000000..80dfd9a2
Binary files /dev/null and b/app/src/gov/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/app/src/gov/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app/src/gov/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
new file mode 100644
index 00000000..2de12545
Binary files /dev/null and b/app/src/gov/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/gov/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/gov/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..badc9f68
Binary files /dev/null and b/app/src/gov/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/gov/res/values/colors.xml b/app/src/gov/res/values/colors.xml
new file mode 100644
index 00000000..1bf8f0c5
--- /dev/null
+++ b/app/src/gov/res/values/colors.xml
@@ -0,0 +1,42 @@
+
+
+
+
+ #A0A0A0
+ #A0A0A0
+ #808080
+ #a0a0a0
+ #D0D0D0
+ #303030
+ #FFFFFF
+ #808080
+ #22B9FF
+ #f0f0f0
+ #202020
+ #C8C8C8
+ #FFFFFF
+ #CF0000
+ #808080
+ #00000000
+ #000000
+ #808080
+ #A0A0A0
+ #303030
+ #505050
+ #606060
+
diff --git a/app/src/gov/res/values/ic_launcher_background.xml b/app/src/gov/res/values/ic_launcher_background.xml
new file mode 100644
index 00000000..beab31f7
--- /dev/null
+++ b/app/src/gov/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #000000
+
\ No newline at end of file
diff --git a/app/src/gov/res/values/strings.xml b/app/src/gov/res/values/strings.xml
new file mode 100644
index 00000000..1f49b610
--- /dev/null
+++ b/app/src/gov/res/values/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ govVPN
+ govVPN
+
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4e8fade6..913ed012 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,13 +1,15 @@
+ xmlns:tools="http://schemas.android.com/tools">
+
+
+
+
+
+
@@ -153,6 +163,11 @@
+
+
() {
+
+ override val layout = R.layout.activity_api_logs
+
+ @Inject
+ protected lateinit var viewModelFactory: ViewModelFactory
+
+ private val viewModel by viewModels { viewModelFactory }
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ EduVPNApplication.get(this).component().inject(this)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ binding.logContents.text = viewModel.getLogFileContents()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/nl/eduvpn/app/MainActivity.kt b/app/src/main/java/nl/eduvpn/app/MainActivity.kt
index 6bd9ebc4..28945ada 100644
--- a/app/src/main/java/nl/eduvpn/app/MainActivity.kt
+++ b/app/src/main/java/nl/eduvpn/app/MainActivity.kt
@@ -120,12 +120,8 @@ class MainActivity : BaseActivity() {
}
is MainViewModel.MainParentAction.ConnectWithConfig -> {
- viewModel.parseConfigAndStartConnection(this, parentAction.config)
- val currentFragment =
- supportFragmentManager.findFragmentById(R.id.content_frame)
- if (currentFragment !is ConnectionStatusFragment) {
- openFragment(ConnectionStatusFragment(), false)
- }
+ viewModel.parseConfigAndStartConnection(this, parentAction.config, parentAction.forceTCP)
+ openFragment(ConnectionStatusFragment(), false)
}
is MainViewModel.MainParentAction.ShowCountriesDialog -> {
@@ -182,7 +178,7 @@ class MainActivity : BaseActivity() {
}
fun selectCountry(cookie: Int? = null) {
- viewModel.getCountryList(this, cookie)
+ viewModel.getCountryList(cookie)
}
private fun openLink(oAuthUrl: String) {
diff --git a/app/src/main/java/nl/eduvpn/app/fragment/SettingsFragment.kt b/app/src/main/java/nl/eduvpn/app/fragment/SettingsFragment.kt
index d376b866..caa7ca55 100644
--- a/app/src/main/java/nl/eduvpn/app/fragment/SettingsFragment.kt
+++ b/app/src/main/java/nl/eduvpn/app/fragment/SettingsFragment.kt
@@ -21,7 +21,10 @@ import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AlertDialog
+import androidx.core.view.isVisible
+import androidx.fragment.app.viewModels
import de.blinkt.openvpn.activities.LogWindow
+import nl.eduvpn.app.ApiLogsActivity
import nl.eduvpn.app.BuildConfig
import nl.eduvpn.app.EduVPNApplication
import nl.eduvpn.app.LicenseActivity
@@ -32,6 +35,7 @@ import nl.eduvpn.app.databinding.FragmentSettingsBinding
import nl.eduvpn.app.entity.Settings
import nl.eduvpn.app.service.HistoryService
import nl.eduvpn.app.service.PreferencesService
+import nl.eduvpn.app.viewmodel.SettingsViewModel
import javax.inject.Inject
/**
@@ -39,18 +43,15 @@ import javax.inject.Inject
* Created by Daniel Zolnai on 2016-10-22.
*/
class SettingsFragment : BaseFragment() {
- @Inject
- lateinit var preferencesService: PreferencesService
- @Inject
- lateinit var historyService: HistoryService
+ val viewModel by viewModels{ viewModelFactory }
override val layout = R.layout.fragment_settings
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
EduVPNApplication.get(view.context).component().inject(this)
- val originalSettings = preferencesService.getAppSettings()
+ val originalSettings = viewModel.appSettings
binding.useCustomTabsSwitch.isChecked = originalSettings.useCustomTabs()
binding.forceTcpSwitch.isChecked = originalSettings.forceTcp()
binding.useCustomTabsSwitch.setOnClickListener { saveSettings() }
@@ -64,10 +65,15 @@ class SettingsFragment : BaseFragment() {
)
}
binding.resetDataButton.setOnClickListener { onResetDataClicked() }
- binding.viewLogButton.setOnClickListener {
+ binding.viewOpenvpnLogsButton.setOnClickListener {
val intent = Intent(activity, LogWindow::class.java)
startActivity(intent)
}
+ binding.viewApiLogsButton.setOnClickListener {
+ val intent = Intent(activity, ApiLogsActivity::class.java)
+ startActivity(intent)
+ }
+ binding.viewApiLogsContainer.isVisible = viewModel.apiLogFile != null
if (!BuildConfig.API_DISCOVERY_ENABLED) {
binding.resetDataSeparator.visibility = View.GONE
binding.resetAppDataContainer.visibility = View.GONE
@@ -75,14 +81,14 @@ class SettingsFragment : BaseFragment() {
}
private fun onResetDataClicked() {
- if (historyService.addedServers?.hasServers() == true) {
+ if (viewModel.hasAddedServers) {
val resetDataDialog = AlertDialog.Builder(requireContext())
.setTitle(R.string.reset_data_dialog_title)
.setMessage(R.string.reset_data_dialog_message)
.setPositiveButton(R.string.reset_data_dialog_yes) { dialog: DialogInterface, _: Int ->
dialog.dismiss()
try {
- historyService.removeOrganizationData()
+ viewModel.removeOrganizationData()
} catch (ex: Exception) {
AlertDialog.Builder(requireContext())
.setTitle(R.string.unexpected_error)
@@ -109,6 +115,6 @@ class SettingsFragment : BaseFragment() {
private fun saveSettings() {
val useCustomTabs = binding.useCustomTabsSwitch.isChecked
val forceTcp = binding.forceTcpSwitch.isChecked
- preferencesService.storeAppSettings(Settings(useCustomTabs, forceTcp))
+ viewModel.storeAppSettings(Settings(useCustomTabs, forceTcp))
}
}
\ No newline at end of file
diff --git a/app/src/main/java/nl/eduvpn/app/inject/EduVPNComponent.kt b/app/src/main/java/nl/eduvpn/app/inject/EduVPNComponent.kt
index 39db792c..a829d7b3 100644
--- a/app/src/main/java/nl/eduvpn/app/inject/EduVPNComponent.kt
+++ b/app/src/main/java/nl/eduvpn/app/inject/EduVPNComponent.kt
@@ -17,6 +17,7 @@
package nl.eduvpn.app.inject
import dagger.Component
+import nl.eduvpn.app.ApiLogsActivity
import nl.eduvpn.app.CertExpiredBroadcastReceiver
import nl.eduvpn.app.DisconnectVPNBroadcastReceiver
import nl.eduvpn.app.EduVPNApplication
@@ -41,6 +42,7 @@ interface EduVPNComponent {
fun inject(organizationSelectionFragment: OrganizationSelectionFragment)
fun inject(mainActivity: MainActivity)
+ fun inject(apiLogsActivity: ApiLogsActivity)
fun inject(connectionStatusFragment: ConnectionStatusFragment)
fun inject(homeFragment: ProfileSelectionFragment)
fun inject(settingsFragment: SettingsFragment)
diff --git a/app/src/main/java/nl/eduvpn/app/inject/ViewModelModule.kt b/app/src/main/java/nl/eduvpn/app/inject/ViewModelModule.kt
index f4d1ecc8..99d50374 100644
--- a/app/src/main/java/nl/eduvpn/app/inject/ViewModelModule.kt
+++ b/app/src/main/java/nl/eduvpn/app/inject/ViewModelModule.kt
@@ -18,6 +18,7 @@
package nl.eduvpn.app.inject
+import android.view.View
import androidx.lifecycle.ViewModel
import dagger.Binds
import dagger.Module
@@ -45,15 +46,25 @@ interface ViewModelModule {
@Binds
@IntoMap
@ViewModelKey(ProfileSelectionViewModel::class)
- fun bindProfileSelectionViewModel(profileSelectionViewModel: ProfileSelectionViewModel) : ViewModel
+ fun bindProfileSelectionViewModel(profileSelectionViewModel: ProfileSelectionViewModel): ViewModel
@Binds
@IntoMap
@ViewModelKey(AddServerViewModel::class)
- fun bindAddServerViewModel(addServerViewModel: AddServerViewModel) : ViewModel
+ fun bindAddServerViewModel(addServerViewModel: AddServerViewModel): ViewModel
@Binds
@IntoMap
@ViewModelKey(MainViewModel::class)
- fun bindMainViewModel(mainViewModel: MainViewModel) : ViewModel
+ fun bindMainViewModel(mainViewModel: MainViewModel): ViewModel
+
+ @Binds
+ @IntoMap
+ @ViewModelKey(SettingsViewModel::class)
+ fun bindSettingsViewModel(settingsViewModel: SettingsViewModel): ViewModel
+
+ @Binds
+ @IntoMap
+ @ViewModelKey(ApiLogsViewModel::class)
+ fun bindApiLogsViewModel(apiLogsViewModel: ApiLogsViewModel): ViewModel
}
diff --git a/app/src/main/java/nl/eduvpn/app/livedata/IPs.kt b/app/src/main/java/nl/eduvpn/app/livedata/IPs.kt
index 84716e73..a3ee86a8 100644
--- a/app/src/main/java/nl/eduvpn/app/livedata/IPs.kt
+++ b/app/src/main/java/nl/eduvpn/app/livedata/IPs.kt
@@ -1,3 +1,4 @@
package nl.eduvpn.app.livedata
-data class IPs(val ipv4: String?, val ipv6: String?)
+data class IPs(val clientIpv4: String?, val clientIpv6: String?, val tunnelData: TunnelData?)
+data class TunnelData(val tunnelIp: String?, val mtu: Int?)
\ No newline at end of file
diff --git a/app/src/main/java/nl/eduvpn/app/livedata/openvpn/IPLiveData.kt b/app/src/main/java/nl/eduvpn/app/livedata/openvpn/IPLiveData.kt
index 13c7454c..468d156b 100644
--- a/app/src/main/java/nl/eduvpn/app/livedata/openvpn/IPLiveData.kt
+++ b/app/src/main/java/nl/eduvpn/app/livedata/openvpn/IPLiveData.kt
@@ -47,13 +47,13 @@ class IPLiveData : LiveData() {
intent: Intent?
) {
if (EduVPNOpenVPNService.connectionStatusToVPNStatus(level) == VPNService.VPNStatus.DISCONNECTED) {
- postValue(IPs(null, null))
+ postValue(IPs(null, null, tunnelData = null))
return
}
// Try to get the address from a lookup
var ips: Pair? = lookupVpnIpAddresses()
if (ips != null) {
- postValue(IPs(ips.first, ips.second))
+ postValue(IPs(ips.first, ips.second, tunnelData = null))
} else {
Log.i(
TAG,
@@ -61,7 +61,7 @@ class IPLiveData : LiveData() {
)
ips = logmessage?.let { lm -> parseVpnIpAddressesFromLogMessage(lm) }
if (ips != null) {
- postValue(IPs(ips.first, ips.second))
+ postValue(IPs(ips.first, ips.second, tunnelData = null))
}
}
}
diff --git a/app/src/main/java/nl/eduvpn/app/service/BackendService.kt b/app/src/main/java/nl/eduvpn/app/service/BackendService.kt
index 00fe4a06..083039c9 100644
--- a/app/src/main/java/nl/eduvpn/app/service/BackendService.kt
+++ b/app/src/main/java/nl/eduvpn/app/service/BackendService.kt
@@ -2,14 +2,19 @@ package nl.eduvpn.app.service
import android.content.Context
import android.net.Uri
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import nl.eduvpn.app.BuildConfig
import nl.eduvpn.app.entity.AddedServers
import nl.eduvpn.app.entity.AuthorizationType
import nl.eduvpn.app.entity.CertExpiryTimes
import nl.eduvpn.app.entity.CurrentServer
import nl.eduvpn.app.entity.Instance
-import nl.eduvpn.app.entity.SerializedVpnConfig
import nl.eduvpn.app.entity.Profile
+import nl.eduvpn.app.entity.SerializedVpnConfig
import nl.eduvpn.app.entity.exception.CommonException
import nl.eduvpn.app.service.SerializerService.UnknownFormatException
import nl.eduvpn.app.utils.Log
@@ -17,6 +22,11 @@ import org.eduvpn.common.GoBackend
import org.eduvpn.common.GoBackend.Callback
import org.eduvpn.common.ServerType
import java.io.File
+import java.net.InetAddress
+import java.net.NetworkInterface
+import java.util.Collections
+import java.util.Locale
+
class BackendService(
private val context: Context,
@@ -43,17 +53,17 @@ class BackendService(
var lastSelectedProfile: String? = null
private set
- private var onConfigReady: ((SerializedVpnConfig) -> Unit)? = null
+ private var onConfigReady: ((SerializedVpnConfig, Boolean) -> Unit)? = null
fun register(
startOAuth: (String) -> Unit,
selectProfiles: (List) -> Unit,
selectCountry: (Int?) -> Unit,
- connectWithConfig: (SerializedVpnConfig) -> Unit,
+ connectWithConfig: (SerializedVpnConfig, Boolean) -> Unit,
showError: (Throwable) -> Unit
): String? {
- onConfigReady = {
- connectWithConfig(it)
+ onConfigReady = { config, forceTcp ->
+ connectWithConfig(config, forceTcp)
}
GoBackend.callbackFunction = object : Callback {
@@ -242,11 +252,11 @@ class BackendService(
}
@kotlin.jvm.Throws(CommonException::class, UnknownFormatException::class)
- suspend fun getConfig(instance: Instance, preferTcp: Boolean) {
+ suspend fun getConfig(instance: Instance, forceTCP: Boolean) = withContext(Dispatchers.IO) {
val dataErrorTuple = goBackend.getProfiles(
instance.authorizationType.toNativeServerType().nativeValue,
instance.baseURI,
- preferTcp,
+ forceTCP,
false
)
@@ -254,9 +264,7 @@ class BackendService(
throw CommonException(dataErrorTuple.error)
}
val config = serializerService.deserializeSerializedVpnConfig(dataErrorTuple.data)
- if (config != null) {
- onConfigReady?.invoke(config)
- }
+ onConfigReady?.invoke(config, forceTCP)
}
@kotlin.jvm.Throws(CommonException::class)
@@ -310,5 +318,64 @@ class BackendService(
pendingOAuthCookie = null
}
}
+
+ fun getLogFile() : File? {
+ val configDirectory = File(context.cacheDir, DIRECTORY_BACKEND_CONFIG_FILES)
+ val configFile = File(configDirectory, "log")
+ if (configFile.exists()) {
+ return configFile
+ }
+ return null
+ }
+
+ /**
+ * Starts checking if there's a stable connection on the tunnel. Only works on WireGuard for now.
+ */
+ suspend fun startFailOver(service: VPNService, onFailOverNeeded: () -> Unit) {
+ goBackend.updateRxBytesRead(0)
+ val updateBytesJob = GlobalScope.launch {
+ service.byteCountFlow.collectLatest {
+ goBackend.updateRxBytesRead(it?.bytesIn ?: 0L)
+ }
+ }
+ service.ipFlow.collectLatest { ips ->
+ val tunnelIp = ips?.tunnelData?.tunnelIp
+ var mtu = ips?.tunnelData?.mtu
+ if (tunnelIp == null) {
+ throw CommonException("Could not start failover, because IP was missing!")
+ }
+ if (mtu == null) {
+ // We could try to get it from the actual network interface
+ try {
+ val tunnelInterface = NetworkInterface.getNetworkInterfaces().toList().firstOrNull {
+ it.inetAddresses.toList().any {
+ if (it.hostAddress != null) {
+ it.hostAddress!! in (ips.clientIpv4?.split(",") ?: emptyList()) ||
+ it.hostAddress!! in (ips.clientIpv6?.split(",") ?: emptyList())
+ } else {
+ false
+ }
+ }
+ }
+ mtu = tunnelInterface?.mtu
+ } catch (ex: Exception) {
+ Log.e(TAG, "Could not determine MTU!", ex)
+ }
+ }
+ if (mtu == null) {
+ throw CommonException("Could not start failover, because MTU was missing!")
+ }
+ Log.v(TAG, "Failover started with tunnel IP: $tunnelIp and MTU: $mtu")
+ val result = goBackend.startFailOver(tunnelIp, mtu)
+ Log.v(TAG, "Failover ended with result: ${result.doesRequireFailover}")
+ updateBytesJob.cancel()
+ if (result.isError) {
+ throw CommonException(result.error)
+ }
+ if (result.doesRequireFailover) {
+ onFailOverNeeded()
+ }
+ }
+ }
}
diff --git a/app/src/main/java/nl/eduvpn/app/service/VPNConnectionService.kt b/app/src/main/java/nl/eduvpn/app/service/VPNConnectionService.kt
index 45f7820c..70deb706 100644
--- a/app/src/main/java/nl/eduvpn/app/service/VPNConnectionService.kt
+++ b/app/src/main/java/nl/eduvpn/app/service/VPNConnectionService.kt
@@ -98,8 +98,8 @@ class VPNConnectionService(
.setOngoing(true)
.setPriority(NotificationCompat.PRIORITY_LOW) // Only used on Android <= 7.1
.addAction(
- R.drawable.ic_menu_close_clear_cancel,
- context.getString(R.string.cancel_connection), disconnectVPNPendingIntent
+ de.blinkt.openvpn.R.drawable.ic_menu_close_clear_cancel,
+ context.getString(de.blinkt.openvpn.R.string.cancel_connection), disconnectVPNPendingIntent
)
.build()
@@ -107,11 +107,8 @@ class VPNConnectionService(
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// Prevent recreating the notification in case we just removed it
- if(statusObserver != null) {
- notificationManager.notify(notificationID, notification)
-
- vpnService.startForeground(notificationID, notification)
- }
+ notificationManager.notify(notificationID, notification)
+ vpnService.startForeground(notificationID, notification)
}
private fun removeVPNNotification(context: Context, vpnService: VPNService) {
diff --git a/app/src/main/java/nl/eduvpn/app/service/WireGuardService.kt b/app/src/main/java/nl/eduvpn/app/service/WireGuardService.kt
index 2230ed14..e67c5953 100644
--- a/app/src/main/java/nl/eduvpn/app/service/WireGuardService.kt
+++ b/app/src/main/java/nl/eduvpn/app/service/WireGuardService.kt
@@ -11,6 +11,7 @@ import com.wireguard.config.Interface
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
@@ -18,11 +19,14 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import nl.eduvpn.app.livedata.ByteCount
import nl.eduvpn.app.livedata.IPs
+import nl.eduvpn.app.livedata.TunnelData
import nl.eduvpn.app.utils.Log
import nl.eduvpn.app.utils.WireGuardTunnel
+import java.net.Inet4Address
import java.util.concurrent.Executors
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
+import kotlin.jvm.optionals.getOrNull
/**
* Service responsible for managing the WireGuard profiles and the connection.
@@ -159,6 +163,33 @@ class WireGuardService(private val context: Context, timer: Flow): VPNServ
}
companion object {
+ private fun calculateTunnelAddress(ipAddress: String?, subnetMask: Int): String? {
+ if (ipAddress == null) {
+ return null
+ }
+ try {
+ val ipParts = ipAddress.split(".")
+ if (ipParts.size != 4) {
+ return null // Invalid IP address format
+ }
+
+ val subnetMaskParts = Array(4) { 0 }
+ for (i in 0 until subnetMask) {
+ subnetMaskParts[i / 8] = subnetMaskParts[i / 8] or (1 shl (7 - i % 8))
+ }
+
+ val networkAddressParts = Array(4) { 0 }
+ for (i in 0 until 4) {
+ networkAddressParts[i] = ipParts[i].toInt() and subnetMaskParts[i]
+ }
+ // Calculate the first valid IP address by adding 1 to the last part of the network address
+ networkAddressParts[3]++
+ return networkAddressParts.joinToString(".")
+ } catch (e: Exception) {
+ return null
+ }
+ }
+
private fun getIPs(wgInterface: Interface): IPs {
val ipv4Addresses = wgInterface.addresses
.filter { network -> network.address is java.net.Inet4Address }
@@ -168,10 +199,19 @@ class WireGuardService(private val context: Context, timer: Flow): VPNServ
.filter { network -> network.address is java.net.Inet6Address }
.mapNotNull { ip -> ip.address.hostAddress }
+ val tunnelIp = wgInterface.addresses
+ .firstOrNull { network -> network.address is Inet4Address }
+ ?.let { ip ->
+ calculateTunnelAddress(ip.address.hostAddress, ip.mask)
+ }
fun ipListToString(ipList: List): String? {
return ipList.reduceOrNull { s1, s2 -> "$s1, $s2" }
}
- return IPs(ipListToString(ipv4Addresses), ipListToString(ipv6Addresses))
+ return IPs(
+ ipListToString(ipv4Addresses),
+ ipListToString(ipv6Addresses),
+ TunnelData(tunnelIp, wgInterface.mtu.getOrNull())
+ )
}
}
}
diff --git a/app/src/main/java/nl/eduvpn/app/utils/LiveEvent.kt b/app/src/main/java/nl/eduvpn/app/utils/LiveEvent.kt
index 8ef82ed8..3664fc5f 100644
--- a/app/src/main/java/nl/eduvpn/app/utils/LiveEvent.kt
+++ b/app/src/main/java/nl/eduvpn/app/utils/LiveEvent.kt
@@ -79,9 +79,9 @@ class LiveEvent : MediatorLiveData() {
private val pending = AtomicBoolean(false)
- override fun onChanged(t: T?) {
+ override fun onChanged(value: T) {
if (pending.compareAndSet(true, false)) {
- observer.onChanged(t)
+ observer.onChanged(value)
}
}
diff --git a/app/src/main/java/nl/eduvpn/app/viewmodel/AddServerViewModel.kt b/app/src/main/java/nl/eduvpn/app/viewmodel/AddServerViewModel.kt
index dcbaa8a3..87d4e05d 100644
--- a/app/src/main/java/nl/eduvpn/app/viewmodel/AddServerViewModel.kt
+++ b/app/src/main/java/nl/eduvpn/app/viewmodel/AddServerViewModel.kt
@@ -20,7 +20,7 @@ package nl.eduvpn.app.viewmodel
import android.content.Context
import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.Transformations
+import androidx.lifecycle.map
import nl.eduvpn.app.service.*
import javax.inject.Inject
@@ -40,7 +40,7 @@ class AddServerViewModel @Inject constructor(
val serverUrl = MutableLiveData("")
- val addButtonEnabled = Transformations.map(serverUrl) { url ->
+ val addButtonEnabled = serverUrl.map { url ->
url != null && url.contains(".") && url.length > 3
}
}
diff --git a/app/src/main/java/nl/eduvpn/app/viewmodel/ApiLogsViewModel.kt b/app/src/main/java/nl/eduvpn/app/viewmodel/ApiLogsViewModel.kt
new file mode 100644
index 00000000..2d3a2ea6
--- /dev/null
+++ b/app/src/main/java/nl/eduvpn/app/viewmodel/ApiLogsViewModel.kt
@@ -0,0 +1,17 @@
+package nl.eduvpn.app.viewmodel
+
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.ViewModel
+import nl.eduvpn.app.entity.Settings
+import nl.eduvpn.app.service.BackendService
+import nl.eduvpn.app.service.HistoryService
+import nl.eduvpn.app.service.PreferencesService
+import javax.inject.Inject
+
+class ApiLogsViewModel @Inject constructor(
+ private val backendService: BackendService
+) : ViewModel() {
+ fun getLogFileContents() : String {
+ return backendService.getLogFile()!!.readLines().joinToString("\n")
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/nl/eduvpn/app/viewmodel/MainViewModel.kt b/app/src/main/java/nl/eduvpn/app/viewmodel/MainViewModel.kt
index 4a627c8f..5c7a9847 100644
--- a/app/src/main/java/nl/eduvpn/app/viewmodel/MainViewModel.kt
+++ b/app/src/main/java/nl/eduvpn/app/viewmodel/MainViewModel.kt
@@ -2,13 +2,12 @@ package nl.eduvpn.app.viewmodel
import android.content.Context
import android.net.Uri
-import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.wireguard.config.Config
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
import nl.eduvpn.app.MainActivity
import nl.eduvpn.app.R
import nl.eduvpn.app.entity.AuthorizationType
@@ -16,12 +15,14 @@ import nl.eduvpn.app.entity.Instance
import nl.eduvpn.app.entity.Profile
import nl.eduvpn.app.entity.SerializedVpnConfig
import nl.eduvpn.app.entity.VPNConfig
+import nl.eduvpn.app.entity.exception.CommonException
import nl.eduvpn.app.service.BackendService
import nl.eduvpn.app.service.EduVPNOpenVPNService
import nl.eduvpn.app.service.HistoryService
import nl.eduvpn.app.service.OrganizationService
import nl.eduvpn.app.service.PreferencesService
import nl.eduvpn.app.service.VPNConnectionService
+import nl.eduvpn.app.utils.Log
import nl.eduvpn.app.utils.getCountryText
import nl.eduvpn.app.utils.toSingleEvent
import org.eduvpn.common.Protocol
@@ -44,11 +45,16 @@ class MainViewModel @Inject constructor(
preferencesService,
vpnConnectionService
) {
+
+ companion object {
+ private val TAG = MainViewModel::class.simpleName
+ }
+
sealed class MainParentAction {
data class OpenLink(val oAuthUrl: String) : MainParentAction()
data class SelectCountry(val cookie: Int?) : MainParentAction()
data class SelectProfiles(val profileList: List): MainParentAction()
- data class ConnectWithConfig(val config: SerializedVpnConfig) : MainParentAction()
+ data class ConnectWithConfig(val config: SerializedVpnConfig, val forceTCP: Boolean) : MainParentAction()
data class ShowCountriesDialog(val instancesWithNames: List>, val cookie: Int?): MainParentAction()
data class ShowError(val throwable: Throwable) : MainParentAction()
}
@@ -67,8 +73,8 @@ class MainViewModel @Inject constructor(
selectProfiles = { profileList ->
_mainParentAction.postValue(MainParentAction.SelectProfiles(profileList))
},
- connectWithConfig = { config ->
- _mainParentAction.postValue(MainParentAction.ConnectWithConfig(config))
+ connectWithConfig = { config, forceTcp ->
+ _mainParentAction.postValue(MainParentAction.ConnectWithConfig(config, forceTcp))
},
showError = { throwable ->
_mainParentAction.postValue(MainParentAction.ShowError(throwable))
@@ -90,7 +96,11 @@ class MainViewModel @Inject constructor(
fun hasServers() = historyService.addedServers?.hasServers() == true
- fun parseConfigAndStartConnection(activity: MainActivity, config: SerializedVpnConfig) {
+ fun parseConfigAndStartConnection(
+ activity: MainActivity,
+ config: SerializedVpnConfig,
+ forceTCP: Boolean
+ ) {
preferencesService.setCurrentProtocol(config.protocol)
val parsedConfig = if (config.protocol == Protocol.OpenVPN.nativeValue) {
eduVpnOpenVpnService.importConfig(
@@ -104,7 +114,31 @@ class MainViewModel @Inject constructor(
} else {
throw IllegalArgumentException("Unexpected protocol type: ${config.protocol}")
}
- vpnConnectionService.connectionToConfig(viewModelScope, activity, parsedConfig)
+ val service = vpnConnectionService.connectionToConfig(viewModelScope, activity, parsedConfig)
+ if (config.protocol == Protocol.WireGuard.nativeValue && !forceTCP) {
+ viewModelScope.launch(Dispatchers.IO) {
+ try {
+ // Waits a bit so that the network interface has been surely set up
+ delay(1_000L)
+ backendService.startFailOver(service) {
+ // Failover needed, request a new profile with TCP enforced.
+ preferencesService.getCurrentInstance()?.let { currentInstance ->
+ viewModelScope.launch {
+ // Disconnect first, otherwise we don't have any internet :)
+ service.disconnect()
+ // Wait a bit for the disconnection to finish
+ delay(500L)
+ // Fetch a new profile, now with TCP forced
+ backendService.getConfig(currentInstance, forceTCP = true)
+ }
+ }
+ }
+ } catch (ex: CommonException) {
+ // These are just warnings, so we log them, but don't display to the user
+ Log.w( TAG, "Unable to start failover detection", ex)
+ }
+ }
+ }
}
suspend fun handleRedirection(data: Uri?) : Boolean {
@@ -114,7 +148,7 @@ class MainViewModel @Inject constructor(
}
fun useCustomTabs() = preferencesService.getAppSettings().useCustomTabs()
- fun getCountryList(activity: MainActivity, cookie: Int? = null) {
+ fun getCountryList(cookie: Int? = null) {
viewModelScope.launch(Dispatchers.IO) {
try {
val allInstances = organizationService.fetchServerList().serverList
diff --git a/app/src/main/java/nl/eduvpn/app/viewmodel/OrganizationSelectionViewModel.kt b/app/src/main/java/nl/eduvpn/app/viewmodel/OrganizationSelectionViewModel.kt
index fa5c2b5e..ed2f1a38 100644
--- a/app/src/main/java/nl/eduvpn/app/viewmodel/OrganizationSelectionViewModel.kt
+++ b/app/src/main/java/nl/eduvpn/app/viewmodel/OrganizationSelectionViewModel.kt
@@ -20,7 +20,6 @@ package nl.eduvpn.app.viewmodel
import android.content.Context
import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.Transformations
import androidx.lifecycle.map
import androidx.lifecycle.switchMap
import androidx.lifecycle.viewModelScope
@@ -234,8 +233,8 @@ class OrganizationSelectionViewModel @Inject constructor(
}
}
- val noItemsFound = Transformations.switchMap(connectionState) { state ->
- Transformations.map(adapterItems) { items ->
+ val noItemsFound = connectionState.switchMap { state ->
+ adapterItems.map { items ->
items.isEmpty() && state == ConnectionState.Ready
}
}
diff --git a/app/src/main/java/nl/eduvpn/app/viewmodel/SettingsViewModel.kt b/app/src/main/java/nl/eduvpn/app/viewmodel/SettingsViewModel.kt
new file mode 100644
index 00000000..67859b2f
--- /dev/null
+++ b/app/src/main/java/nl/eduvpn/app/viewmodel/SettingsViewModel.kt
@@ -0,0 +1,30 @@
+package nl.eduvpn.app.viewmodel
+
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.ViewModel
+import nl.eduvpn.app.entity.Settings
+import nl.eduvpn.app.service.BackendService
+import nl.eduvpn.app.service.HistoryService
+import nl.eduvpn.app.service.PreferencesService
+import javax.inject.Inject
+
+class SettingsViewModel @Inject constructor(
+ private val historyService: HistoryService,
+ private val preferencesService: PreferencesService,
+ private val backendService: BackendService
+) : ViewModel() {
+
+ val appSettings get() = preferencesService.getAppSettings()
+
+ val apiLogFile get() = backendService.getLogFile()
+
+ val hasAddedServers get() = historyService.addedServers?.hasServers() == true
+
+ fun removeOrganizationData() {
+ historyService.removeOrganizationData()
+ }
+
+ fun storeAppSettings(appSettings: Settings) {
+ preferencesService.storeAppSettings(appSettings)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_api_logs.xml b/app/src/main/res/layout/activity_api_logs.xml
new file mode 100644
index 00000000..8295b34c
--- /dev/null
+++ b/app/src/main/res/layout/activity_api_logs.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_connection_status.xml b/app/src/main/res/layout/fragment_connection_status.xml
index 134fdc29..3885c894 100644
--- a/app/src/main/res/layout/fragment_connection_status.xml
+++ b/app/src/main/res/layout/fragment_connection_status.xml
@@ -316,7 +316,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
- android:text="@{ips.ipv4 ?? @string/not_available}"
+ android:text="@{ips.clientIpv4 ?? @string/not_available}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/label_ipv4"
tools:text="123.123.123" />
@@ -338,7 +338,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
- android:text="@{ips.ipv6 ?? @string/not_available}"
+ android:text="@{ips.clientIpv6 ?? @string/not_available}"
app:layout_constraintLeft_toLeftOf="@id/guide_vertical_divide"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/label_ipv6"
diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml
index 3301050f..9fd98056 100644
--- a/app/src/main/res/layout/fragment_settings.xml
+++ b/app/src/main/res/layout/fragment_settings.xml
@@ -91,19 +91,19 @@
android:layout_below="@id/useCustomTabsSwitch" />
+ android:text="@string/settings_view_openvpn_logs_title" />
Zertifikat abgelaufen
Die Verbindung wurde getrennt, weil das Zertifikat abgelaufen ist. Verbinden Sie sich neu, um sich wieder zu autorisieren.
- Protokoll zeigen
- Verbindungsprotokoll
+ Protokollen zeigen
Keine Organization
OK
Sie haben keine verbundene Organisation.
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 42c80dad..5bbfaaf1 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -178,7 +178,6 @@
Certificado expirado
Fué desconectado porque el certificado expiró. Conéctese de nuevo para volver a autenticarse.
Ver Registro
- Registro de conexión
Sin organización
OK
No tienes una organización conectada.
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 01c1952a..abda0ffa 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -179,8 +179,9 @@
%1$s and %2$s]]>
Certificate expired
You were disconnected because the certificate expired. Connect again to reauthenticate.
- View Log
- Connection log
+ View Logs
+ OpenVPN logs
+ API logs
No organization
OK
You have no connected organization.
diff --git a/build.gradle b/build.gradle
index 99d7eed4..27c80b0b 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,7 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
- ext.kotlin_version = '1.8.0'
+ ext.kotlin_version = '1.9.10'
repositories {
maven {
url "https://maven.google.com"
@@ -9,7 +9,7 @@ buildscript {
jcenter()
}
dependencies {
- classpath "com.android.tools.build:gradle:7.3.1"
+ classpath "com.android.tools.build:gradle:8.1.2"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
diff --git a/common/build.gradle b/common/build.gradle
index 40363969..2fbd4441 100644
--- a/common/build.gradle
+++ b/common/build.gradle
@@ -5,7 +5,7 @@ plugins {
android {
namespace 'org.eduvpn.common'
compileSdk 33
- ndkVersion = "25.1.8937393"
+ ndkVersion = "26.1.10909125"
def commonVersion = "2.0.0"
@@ -32,7 +32,6 @@ android {
externalNativeBuild {
cmake {
path "libs/CMakeLists.txt"
- version "3.27.7"
}
}
compileOptions {
@@ -52,10 +51,5 @@ android {
}
dependencies {
-
- implementation 'androidx.appcompat:appcompat:1.6.1'
- implementation 'com.google.android.material:material:1.9.0'
- testImplementation 'junit:junit:4.13.2'
- androidTestImplementation 'androidx.test.ext:junit:1.1.5'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
+ implementation 'androidx.annotation:annotation-jvm:1.7.0'
}
diff --git a/common/consumer-rules.pro b/common/consumer-rules.pro
index e69de29b..3652412b 100644
--- a/common/consumer-rules.pro
+++ b/common/consumer-rules.pro
@@ -0,0 +1 @@
+-keep public class org.eduvpn.common.** { *; }
\ No newline at end of file
diff --git a/common/libs/eduvpn-common b/common/libs/eduvpn-common
index 333f7487..79835d32 160000
--- a/common/libs/eduvpn-common
+++ b/common/libs/eduvpn-common
@@ -1 +1 @@
-Subproject commit 333f7487985fac50104e86afb4e2d77ce94d3597
+Subproject commit 79835d32239b44fce825f20642935afef3f1d92e
diff --git a/common/src/main/cpp/jni.cpp b/common/src/main/cpp/jni.cpp
index 00ffe383..76523d99 100644
--- a/common/src/main/cpp/jni.cpp
+++ b/common/src/main/cpp/jni.cpp
@@ -10,6 +10,7 @@ static JavaVM *globalVM;
static jclass globalBackendClass;
static jclass globalCallbackClass;
+static jlong globalRxBytesRead = 0;
bool GetJniEnv(JavaVM *vm, JNIEnv **env) {
bool did_attach_thread = false;
@@ -46,6 +47,10 @@ jobject CreateDataErrorTuple(JNIEnv *env, char *data, char *error) {
return object;
}
+long long int getRxBytesRead() {
+ return globalRxBytesRead;
+}
+
int callGlobalCallback(int newstate, void *data) {
if (!globalVM) {
return 0;
@@ -77,6 +82,8 @@ int createStateCallback(int oldstate, int newstate, void *data) {
return callGlobalCallback(newstate, data);
}
+
+
void getToken(const char* server, char* out, size_t len) {
if (!globalVM) {
return;
@@ -252,7 +259,7 @@ Java_org_eduvpn_common_GoBackend_selectCountry(JNIEnv *env, jobject /* this */,
uintptr_t cookie = CookieNew();
const char *countryCode_str = env->GetStringUTFChars(countryCode, nullptr);
char *result = SetSecureLocation(cookie, (char *)countryCode_str);
- CookieCancel(cookie);
+ CookieDelete(cookie);
return NativeStringToJString(env, result);
}
@@ -260,4 +267,23 @@ extern "C" JNIEXPORT jobject JNICALL
Java_org_eduvpn_common_GoBackend_getCertExpiryTimes(JNIEnv *env, jobject /* this */) {
ExpiryTimes_return result = ExpiryTimes();
return CreateDataErrorTuple(env, result.r0, result.r1);
+}
+extern "C"
+JNIEXPORT jobject JNICALL
+Java_org_eduvpn_common_GoBackend_startFailOver(JNIEnv *env, jobject /* this */, jstring gatewayIp, jint mtu) {
+ uintptr_t cookie = CookieNew();
+ const char *gatewayIp_str = env->GetStringUTFChars(gatewayIp, nullptr);
+ StartFailover_return result = StartFailover(cookie, (char *)gatewayIp_str, (int)mtu, getRxBytesRead);
+ CookieDelete(cookie);
+ jboolean failOverNeeded = result.r0 != 0;
+ jstring errorString = NativeStringToJString(env, result.r1);
+ jclass failOverCls = env->FindClass("org/eduvpn/common/FailoverResult");
+ jmethodID constructor = env->GetMethodID(failOverCls, "", "(ZLjava/lang/String;)V");
+ jobject object = env->NewObject( failOverCls, constructor, failOverNeeded, errorString);
+ return object;
+}
+extern "C"
+JNIEXPORT void JNICALL
+Java_org_eduvpn_common_GoBackend_updateRxBytesRead(JNIEnv *env, jobject /* this */, jlong rxBytesRead) {
+ globalRxBytesRead = rxBytesRead;
}
\ No newline at end of file
diff --git a/common/src/main/java/org/eduvpn/common/FailoverResult.java b/common/src/main/java/org/eduvpn/common/FailoverResult.java
new file mode 100644
index 00000000..fde041c7
--- /dev/null
+++ b/common/src/main/java/org/eduvpn/common/FailoverResult.java
@@ -0,0 +1,16 @@
+package org.eduvpn.common;
+
+import androidx.annotation.Nullable;
+
+public class FailoverResult {
+ public final boolean doesRequireFailover;
+ public final @Nullable String error;
+
+ FailoverResult(boolean doesRequireFailover, @Nullable String error) {
+ this.doesRequireFailover = doesRequireFailover;
+ this.error = error;
+ }
+ public boolean isError() {
+ return error != null;
+ }
+}
diff --git a/common/src/main/java/org/eduvpn/common/GoBackend.java b/common/src/main/java/org/eduvpn/common/GoBackend.java
index 0b6b9d20..c422099c 100644
--- a/common/src/main/java/org/eduvpn/common/GoBackend.java
+++ b/common/src/main/java/org/eduvpn/common/GoBackend.java
@@ -40,4 +40,6 @@ public interface Callback {
public native @Nullable String cancelCookie(int cookie);
public native @Nullable String deregister();
public native DataErrorTuple getCertExpiryTimes();
+ public native void updateRxBytesRead(long rxBytesRead);
+ public native FailoverResult startFailOver(@NotNull String gatewayIp, int mtu);
}
\ No newline at end of file
diff --git a/common/src/main/java/org/eduvpn/common/StateCB.java b/common/src/main/java/org/eduvpn/common/StateCB.java
deleted file mode 100644
index d29d0ce6..00000000
--- a/common/src/main/java/org/eduvpn/common/StateCB.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package org.eduvpn.common;
-
-import androidx.annotation.Nullable;
-
-public class StateCB {
- public final int oldState;
- public final int newState;
- @Nullable
- public final Object data;
-
- public StateCB(int oldState, int newState, @Nullable Object data) {
- this.oldState = oldState;
- this.newState = newState;
- this.data = data;
- }
-}
diff --git a/gradle.properties b/gradle.properties
index 7fa8ce04..fe661bf8 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -20,3 +20,4 @@ org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true
+android.defaults.buildfeatures.buildconfig=true
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index aa991fce..fae08049 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/ics-openvpn b/ics-openvpn
index 54a25570..fe0f4708 160000
--- a/ics-openvpn
+++ b/ics-openvpn
@@ -1 +1 @@
-Subproject commit 54a2557059a67a0f07bde49d9718595c150d6b67
+Subproject commit fe0f4708f605a780fad4104bc013c345b0fa2adf
diff --git a/settings.gradle b/settings.gradle
index 91191c41..2356682d 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,4 +1,20 @@
+// The following block is needed because of this issue: https://stackoverflow.com/a/76954759/1395437
+// Can be removed when upgrading to Android Studio Hedgehog
+pluginManagement {
+ buildscript {
+ repositories {
+ mavenCentral()
+ maven {
+ url = uri("https://storage.googleapis.com/r8-releases/raw")
+ }
+ }
+ dependencies {
+ classpath("com.android.tools:r8:8.2.24")
+ }
+ }
+}
+
include ':app'
include ':ics-openvpn-main'
project(':ics-openvpn-main').projectDir = new File(rootDir, '/ics-openvpn/main')
-include ':common'
+include ':common'
\ No newline at end of file