Compare commits

..

3 Commits

Author SHA1 Message Date
Zane Schepke 205493092b fix: cd 2024-10-22 01:23:35 -04:00
Zane Schepke 47472f088f bump version
revert some ui changes
2024-10-22 00:49:34 -04:00
Zane Schepke f5a62cba1b fix: preshared key password field
closes #405
2024-10-22 00:09:19 -04:00
281 changed files with 2851 additions and 5338 deletions
+13 -42
View File
@@ -34,28 +34,28 @@ on:
jobs: jobs:
check_commits: check_commits:
name: Check for New Commits
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs: outputs:
has_new_commits: ${{ steps.check.outputs.new_commits }} new_commits: ${{ steps.check_last_commit.outputs.new_commits }}
steps: steps:
- name: Checkout Repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
fetch-depth: 0 # This fetches all history so we can check commits fetch-depth: 0
- name: Check for new commits - name: Check for new commits in the last 23 hours
id: check id: check_last_commit
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
# This script checks for commits newer than 23 hours ago if git log --since="23 hours ago" --oneline | grep -q .; then
NEW_COMMITS=$(git rev-list --count --after="$(date -Iseconds -d '23 hours ago')" ${{ github.sha }}) echo "New commits found in the last 23 hours."
echo "new_commits=$NEW_COMMITS" >> $GITHUB_OUTPUT echo "new_commits=true" >> $GITHUB_OUTPUT
else
echo "No new commits in the last 23 hours."
echo "new_commits=false" >> $GITHUB_OUTPUT
fi
build: build:
needs: check_commits needs: check_commits
if: ${{ needs.check_commits.outputs.has_new_commits > 0 && inputs.release_type != 'none' }} if: needs.check_commits.outputs.new_commits == 'true'
name: Build Signed APK name: Build Signed APK
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
@@ -94,32 +94,6 @@ jobs:
fileDir: ${{ env.KEY_STORE_LOCATION }} fileDir: ${{ env.KEY_STORE_LOCATION }}
encodedString: ${{ secrets.KEYSTORE }} encodedString: ${{ secrets.KEYSTORE }}
# update latest tag
- name: Set latest tag
uses: rickstaa/action-create-tag@v1
id: tag_creation
with:
tag: "latest" # or any tag name you wish to use
message: "Automated tag for HEAD commit"
force_push_tag: true
tag_exists_error: false
- name: Get latest release
id: latest_release
uses: kaliber5/action-get-release@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
latest: true
- name: Generate Changelog
id: changelog
uses: requarks/changelog-action@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
toTag: ${{ github.event_name == 'schedule' && 'nightly' || steps.latest_release.outputs.tag_name }}
fromTag: "latest"
writeToFile: false # we won't write to file, just output
# create keystore path for gradle to read # create keystore path for gradle to read
- name: Create keystore path env var - name: Create keystore path env var
run: | run: |
@@ -235,9 +209,6 @@ jobs:
SHA256 fingerprint: SHA256 fingerprint:
```${{ steps.checksum.outputs.checksum }}``` ```${{ steps.checksum.outputs.checksum }}```
### Changelog
${{ steps.changelog.outputs.changes }}
tag_name: ${{ env.TAG_NAME }} tag_name: ${{ env.TAG_NAME }}
name: ${{ env.TAG_NAME }} name: ${{ env.TAG_NAME }}
draft: false draft: false
+1
View File
@@ -5,6 +5,7 @@ WG Tunnel
<div align="center"> <div align="center">
[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/rbRRNh6H7V) [![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/rbRRNh6H7V)
[![X Community](https://img.shields.io/badge/X-000000?style=for-the-badge&logo=x&logoColor=white)](https://twitter.com/i/communities/1780655267685736818)
[![Telegram](https://img.shields.io/badge/Telegram-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/wgtunnel) [![Telegram](https://img.shields.io/badge/Telegram-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/wgtunnel)
</div> </div>
-3
View File
@@ -44,8 +44,6 @@ android {
getByName("debug").assets.srcDirs(files("$projectDir/schemas")) // Room getByName("debug").assets.srcDirs(files("$projectDir/schemas")) // Room
} }
buildConfigField("String[]", "LANGUAGES", "new String[]{ ${languageList().joinToString(separator = ", ") { "\"$it\"" }} }")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { useSupportLibrary = true } vectorDrawables { useSupportLibrary = true }
} }
@@ -151,7 +149,6 @@ dependencies {
implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.material3) implementation(libs.androidx.material3)
implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat)
implementation(libs.material)
// test // test
testImplementation(libs.junit) testImplementation(libs.junit)
@@ -1,232 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 11,
"identityHash": "4c9418386f72dfac5d28ab96c1e5ea0b",
"entities": [
{
"tableName": "Settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT false, `is_wifi_by_shell_enabled` INTEGER NOT NULL DEFAULT false)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelEnabled",
"columnName": "is_kernel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isAmneziaEnabled",
"columnName": "is_amnezia_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWildcardsEnabled",
"columnName": "is_wildcards_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWifiNameByShellEnabled",
"columnName": "is_wifi_by_shell_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "TunnelConfig",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '', `is_Active` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `ping_interval` INTEGER DEFAULT null, `ping_cooldown` INTEGER DEFAULT null, `ping_ip` TEXT DEFAULT null)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wgQuick",
"columnName": "wg_quick",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tunnelNetworks",
"columnName": "tunnel_networks",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isMobileDataTunnel",
"columnName": "is_mobile_data_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPrimaryTunnel",
"columnName": "is_primary_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "amQuick",
"columnName": "am_quick",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isActive",
"columnName": "is_Active",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "pingInterval",
"columnName": "ping_interval",
"affinity": "INTEGER",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "pingCooldown",
"columnName": "ping_cooldown",
"affinity": "INTEGER",
"notNull": false,
"defaultValue": "null"
},
{
"fieldPath": "pingIp",
"columnName": "ping_ip",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "null"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_TunnelConfig_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4c9418386f72dfac5d28ab96c1e5ea0b')"
]
}
}
+13 -12
View File
@@ -3,6 +3,13 @@
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
@@ -12,8 +19,7 @@
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!--foreground service exempt android 14--> <!--foreground service exempt android 14-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
tools:ignore="ProtectedPermissions" />
<!--foreground service permissions--> <!--foreground service permissions-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
@@ -40,12 +46,6 @@
<uses-feature <uses-feature
android:name="android.hardware.screen.portrait" android:name="android.hardware.screen.portrait"
android:required="false" /> android:required="false" />
<uses-feature
android:name="android.hardware.gamepad"
android:required="false"/>
<uses-feature android:name="android.hardware.wifi"
android:required="false"/>
<queries> <queries>
<intent> <intent>
@@ -68,9 +68,7 @@
<activity <activity
android:name=".ui.MainActivity" android:name=".ui.MainActivity"
android:exported="true" android:exported="true"
android:theme="@style/Theme.WireguardAutoTunnel" android:theme="@style/Theme.WireguardAutoTunnel">
android:configChanges="orientation|screenSize|keyboardHidden"
>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@@ -168,7 +166,6 @@
<service <service
android:name=".service.foreground.TunnelBackgroundService" android:name=".service.foreground.TunnelBackgroundService"
android:exported="false" android:exported="false"
android:persistent="true"
android:foregroundServiceType="systemExempted" android:foregroundServiceType="systemExempted"
android:permission="android.permission.BIND_VPN_SERVICE"> android:permission="android.permission.BIND_VPN_SERVICE">
<intent-filter> <intent-filter>
@@ -189,6 +186,10 @@
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" /> <action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver
android:name=".receiver.BackgroundActionReceiver"
android:enabled="true"
android:exported="false"/>
<receiver <receiver
android:name=".receiver.AppUpdateReceiver" android:name=".receiver.AppUpdateReceiver"
android:exported="false"> android:exported="false">
@@ -3,13 +3,9 @@ package com.zaneschepke.wireguardautotunnel
import android.app.Application import android.app.Application
import android.os.StrictMode import android.os.StrictMode
import android.os.StrictMode.ThreadPolicy import android.os.StrictMode.ThreadPolicy
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
import com.zaneschepke.logcatter.LogReader import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
@@ -29,9 +25,6 @@ class WireGuardAutoTunnel : Application() {
@Inject @Inject
lateinit var logReader: LogReader lateinit var logReader: LogReader
@Inject
lateinit var appStateRepository: AppStateRepository
@Inject @Inject
@IoDispatcher @IoDispatcher
lateinit var ioDispatcher: CoroutineDispatcher lateinit var ioDispatcher: CoroutineDispatcher
@@ -52,19 +45,9 @@ class WireGuardAutoTunnel : Application() {
} else { } else {
Timber.plant(ReleaseTree()) Timber.plant(ReleaseTree())
} }
applicationScope.launch {
appStateRepository.getLocale()?.let {
val locale = LocaleUtil.getLocaleFromPrefCode(it)
val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(locale)
AppCompatDelegate.setApplicationLocales(appLocale)
}
}
if (!isRunningOnTv()) { if (!isRunningOnTv()) {
applicationScope.launch(ioDispatcher) { applicationScope.launch(ioDispatcher) {
if (appStateRepository.isLocalLogsEnabled()) { logReader.start()
Timber.d("Starting logger")
logReader.start()
}
} }
} }
} }
@@ -11,7 +11,7 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
@Database( @Database(
entities = [Settings::class, TunnelConfig::class], entities = [Settings::class, TunnelConfig::class],
version = 11, version = 10,
autoMigrations = autoMigrations =
[ [
AutoMigration(from = 1, to = 2), AutoMigration(from = 1, to = 2),
@@ -36,11 +36,6 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
AutoMigration(7, 8), AutoMigration(7, 8),
AutoMigration(8, 9), AutoMigration(8, 9),
AutoMigration(9, 10), AutoMigration(9, 10),
AutoMigration(
from = 10,
to = 11,
spec = RemoveTunnelPauseMigration::class,
),
], ],
exportSchema = true, exportSchema = true,
) )
@@ -60,9 +55,3 @@ abstract class AppDatabase : RoomDatabase() {
columnName = "is_battery_saver_enabled", columnName = "is_battery_saver_enabled",
) )
class RemoveLegacySettingColumnsMigration : AutoMigrationSpec class RemoveLegacySettingColumnsMigration : AutoMigrationSpec
@DeleteColumn(
tableName = "Settings",
columnName = "is_auto_tunnel_paused",
)
class RemoveTunnelPauseMigration : AutoMigrationSpec
@@ -21,14 +21,11 @@ class DataStoreManager(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher, @IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) { ) {
companion object { companion object {
val locationDisclosureShown = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN") val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
val batteryDisableShown = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN") val BATTERY_OPTIMIZE_DISABLE_SHOWN = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
val currentSSID = stringPreferencesKey("CURRENT_SSID") val CURRENT_SSID = stringPreferencesKey("CURRENT_SSID")
val pinLockEnabled = booleanPreferencesKey("PIN_LOCK_ENABLED") val IS_PIN_LOCK_ENABLED = booleanPreferencesKey("PIN_LOCK_ENABLED")
val tunnelStatsExpanded = booleanPreferencesKey("TUNNEL_STATS_EXPANDED") val IS_TUNNEL_STATS_EXPANDED = booleanPreferencesKey("TUNNEL_STATS_EXPANDED")
val isLocalLogsEnabled = booleanPreferencesKey("LOCAL_LOGS_ENABLED")
val locale = stringPreferencesKey("LOCALE")
val theme = stringPreferencesKey("THEME")
} }
// preferences // preferences
@@ -1,21 +1,15 @@
package com.zaneschepke.wireguardautotunnel.data.domain package com.zaneschepke.wireguardautotunnel.data.domain
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
data class GeneralState( data class GeneralState(
val isLocationDisclosureShown: Boolean = LOCATION_DISCLOSURE_SHOWN_DEFAULT, val isLocationDisclosureShown: Boolean = LOCATION_DISCLOSURE_SHOWN_DEFAULT,
val isBatteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT, val isBatteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
val isPinLockEnabled: Boolean = PIN_LOCK_ENABLED_DEFAULT, val isPinLockEnabled: Boolean = PIN_LOCK_ENABLED_DEFAULT,
val isTunnelStatsExpanded: Boolean = IS_TUNNEL_STATS_EXPANDED, val isTunnelStatsExpanded: Boolean = IS_TUNNEL_STATS_EXPANDED,
val isLocalLogsEnabled: Boolean = IS_LOGS_ENABLED_DEFAULT,
val locale: String? = null,
val theme: Theme = Theme.AUTOMATIC,
) { ) {
companion object { companion object {
const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = false const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = false
const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false
const val PIN_LOCK_ENABLED_DEFAULT = false const val PIN_LOCK_ENABLED_DEFAULT = false
const val IS_TUNNEL_STATS_EXPANDED = false const val IS_TUNNEL_STATS_EXPANDED = false
const val IS_LOGS_ENABLED_DEFAULT = false
} }
} }
@@ -40,6 +40,11 @@ data class Settings(
defaultValue = "false", defaultValue = "false",
) )
val isMultiTunnelEnabled: Boolean = false, val isMultiTunnelEnabled: Boolean = false,
@ColumnInfo(
name = "is_auto_tunnel_paused",
defaultValue = "false",
)
val isAutoTunnelPaused: Boolean = false,
@ColumnInfo( @ColumnInfo(
name = "is_ping_enabled", name = "is_ping_enabled",
defaultValue = "false", defaultValue = "false",
@@ -50,14 +55,4 @@ data class Settings(
defaultValue = "false", defaultValue = "false",
) )
val isAmneziaEnabled: Boolean = false, val isAmneziaEnabled: Boolean = false,
@ColumnInfo(
name = "is_wildcards_enabled",
defaultValue = "false",
)
val isWildcardsEnabled: Boolean = false,
@ColumnInfo(
name = "is_wifi_by_shell_enabled",
defaultValue = "false",
)
val isWifiNameByShellEnabled: Boolean = false,
) )
@@ -5,7 +5,6 @@ import androidx.room.Entity
import androidx.room.Index import androidx.room.Index
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.wireguard.config.Config import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.util.extensions.toWgQuickString
import java.io.InputStream import java.io.InputStream
@Entity(indices = [Index(value = ["name"], unique = true)]) @Entity(indices = [Index(value = ["name"], unique = true)])
@@ -80,12 +79,6 @@ data class TunnelConfig(
} }
} }
fun tunnelConfigFromAmConfig(config: org.amnezia.awg.config.Config, name: String): TunnelConfig {
val amQuick = config.toAwgQuickString(true)
val wgQuick = config.toWgQuickString()
return TunnelConfig(name = name, wgQuick = wgQuick, amQuick = amQuick)
}
const val AM_QUICK_DEFAULT = "" const val AM_QUICK_DEFAULT = ""
} }
} }
@@ -1,7 +1,6 @@
package com.zaneschepke.wireguardautotunnel.data.repository package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface AppStateRepository { interface AppStateRepository {
@@ -25,17 +24,5 @@ interface AppStateRepository {
suspend fun setTunnelStatsExpanded(expanded: Boolean) suspend fun setTunnelStatsExpanded(expanded: Boolean)
suspend fun setTheme(theme: Theme)
suspend fun getTheme(): Theme
suspend fun isLocalLogsEnabled(): Boolean
suspend fun setLocalLogsEnabled(enabled: Boolean)
suspend fun setLocale(localeTag: String)
suspend fun getLocale(): String?
val generalStateFlow: Flow<GeneralState> val generalStateFlow: Flow<GeneralState>
} }
@@ -2,7 +2,6 @@ package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import timber.log.Timber import timber.log.Timber
@@ -12,77 +11,47 @@ class DataStoreAppStateRepository(
) : ) :
AppStateRepository { AppStateRepository {
override suspend fun isLocationDisclosureShown(): Boolean { override suspend fun isLocationDisclosureShown(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.locationDisclosureShown) return dataStoreManager.getFromStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN)
?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT ?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT
} }
override suspend fun setLocationDisclosureShown(shown: Boolean) { override suspend fun setLocationDisclosureShown(shown: Boolean) {
dataStoreManager.saveToDataStore(DataStoreManager.locationDisclosureShown, shown) dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, shown)
} }
override suspend fun isPinLockEnabled(): Boolean { override suspend fun isPinLockEnabled(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.pinLockEnabled) return dataStoreManager.getFromStore(DataStoreManager.IS_PIN_LOCK_ENABLED)
?: GeneralState.PIN_LOCK_ENABLED_DEFAULT ?: GeneralState.PIN_LOCK_ENABLED_DEFAULT
} }
override suspend fun setPinLockEnabled(enabled: Boolean) { override suspend fun setPinLockEnabled(enabled: Boolean) {
dataStoreManager.saveToDataStore(DataStoreManager.pinLockEnabled, enabled) dataStoreManager.saveToDataStore(DataStoreManager.IS_PIN_LOCK_ENABLED, enabled)
} }
override suspend fun isBatteryOptimizationDisableShown(): Boolean { override suspend fun isBatteryOptimizationDisableShown(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.batteryDisableShown) return dataStoreManager.getFromStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN)
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT ?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT
} }
override suspend fun setBatteryOptimizationDisableShown(shown: Boolean) { override suspend fun setBatteryOptimizationDisableShown(shown: Boolean) {
dataStoreManager.saveToDataStore(DataStoreManager.batteryDisableShown, shown) dataStoreManager.saveToDataStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN, shown)
} }
override suspend fun getCurrentSsid(): String? { override suspend fun getCurrentSsid(): String? {
return dataStoreManager.getFromStore(DataStoreManager.currentSSID) return dataStoreManager.getFromStore(DataStoreManager.CURRENT_SSID)
} }
override suspend fun setCurrentSsid(ssid: String) { override suspend fun setCurrentSsid(ssid: String) {
dataStoreManager.saveToDataStore(DataStoreManager.currentSSID, ssid) dataStoreManager.saveToDataStore(DataStoreManager.CURRENT_SSID, ssid)
} }
override suspend fun isTunnelStatsExpanded(): Boolean { override suspend fun isTunnelStatsExpanded(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.tunnelStatsExpanded) return dataStoreManager.getFromStore(DataStoreManager.IS_TUNNEL_STATS_EXPANDED)
?: GeneralState.IS_TUNNEL_STATS_EXPANDED ?: GeneralState.IS_TUNNEL_STATS_EXPANDED
} }
override suspend fun setTunnelStatsExpanded(expanded: Boolean) { override suspend fun setTunnelStatsExpanded(expanded: Boolean) {
dataStoreManager.saveToDataStore(DataStoreManager.tunnelStatsExpanded, expanded) dataStoreManager.saveToDataStore(DataStoreManager.IS_TUNNEL_STATS_EXPANDED, expanded)
}
override suspend fun setTheme(theme: Theme) {
dataStoreManager.saveToDataStore(DataStoreManager.theme, theme.name)
}
override suspend fun getTheme(): Theme {
return dataStoreManager.getFromStore(DataStoreManager.theme)?.let {
try {
Theme.valueOf(it)
} catch (_: IllegalArgumentException) {
Theme.AUTOMATIC
}
} ?: Theme.AUTOMATIC
}
override suspend fun isLocalLogsEnabled(): Boolean {
return dataStoreManager.getFromStore(DataStoreManager.isLocalLogsEnabled) ?: GeneralState.IS_LOGS_ENABLED_DEFAULT
}
override suspend fun setLocalLogsEnabled(enabled: Boolean) {
dataStoreManager.saveToDataStore(DataStoreManager.isLocalLogsEnabled, enabled)
}
override suspend fun setLocale(localeTag: String) {
dataStoreManager.saveToDataStore(DataStoreManager.locale, localeTag)
}
override suspend fun getLocale(): String? {
return dataStoreManager.getFromStore(DataStoreManager.locale)
} }
override val generalStateFlow: Flow<GeneralState> = override val generalStateFlow: Flow<GeneralState> =
@@ -91,18 +60,15 @@ class DataStoreAppStateRepository(
try { try {
GeneralState( GeneralState(
isLocationDisclosureShown = isLocationDisclosureShown =
pref[DataStoreManager.locationDisclosureShown] pref[DataStoreManager.LOCATION_DISCLOSURE_SHOWN]
?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT, ?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT,
isBatteryOptimizationDisableShown = isBatteryOptimizationDisableShown =
pref[DataStoreManager.batteryDisableShown] pref[DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN]
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT, ?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
isPinLockEnabled = isPinLockEnabled =
pref[DataStoreManager.pinLockEnabled] pref[DataStoreManager.IS_PIN_LOCK_ENABLED]
?: GeneralState.PIN_LOCK_ENABLED_DEFAULT, ?: GeneralState.PIN_LOCK_ENABLED_DEFAULT,
isTunnelStatsExpanded = pref[DataStoreManager.tunnelStatsExpanded] ?: GeneralState.IS_TUNNEL_STATS_EXPANDED, isTunnelStatsExpanded = pref[DataStoreManager.IS_TUNNEL_STATS_EXPANDED] ?: GeneralState.IS_TUNNEL_STATS_EXPANDED,
isLocalLogsEnabled = pref[DataStoreManager.isLocalLogsEnabled] ?: GeneralState.IS_LOGS_ENABLED_DEFAULT,
locale = pref[DataStoreManager.locale],
theme = getTheme(),
) )
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
Timber.e(e) Timber.e(e)
@@ -1,9 +1,11 @@
package com.zaneschepke.wireguardautotunnel.data.repository package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -24,6 +26,8 @@ class RoomTunnelConfigRepository(
override suspend fun save(tunnelConfig: TunnelConfig) { override suspend fun save(tunnelConfig: TunnelConfig) {
withContext(ioDispatcher) { withContext(ioDispatcher) {
tunnelConfigDao.save(tunnelConfig) tunnelConfigDao.save(tunnelConfig)
}.also {
WireGuardAutoTunnel.instance.requestTunnelTileServiceStateUpdate()
} }
} }
@@ -56,6 +60,8 @@ class RoomTunnelConfigRepository(
override suspend fun delete(tunnelConfig: TunnelConfig) { override suspend fun delete(tunnelConfig: TunnelConfig) {
withContext(ioDispatcher) { withContext(ioDispatcher) {
tunnelConfigDao.delete(tunnelConfig) tunnelConfigDao.delete(tunnelConfig)
}.also {
WireGuardAutoTunnel.instance.requestTunnelTileServiceStateUpdate()
} }
} }
@@ -9,11 +9,3 @@ annotation class Kernel
@Qualifier @Qualifier
@Retention(AnnotationRetention.BINARY) @Retention(AnnotationRetention.BINARY)
annotation class Userspace annotation class Userspace
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class TunnelShell
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AppShell
@@ -9,7 +9,6 @@ import com.wireguard.android.util.RootShell
import com.wireguard.android.util.ToolsInstaller import com.wireguard.android.util.ToolsInstaller
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel
import dagger.Module import dagger.Module
@@ -25,18 +24,9 @@ import javax.inject.Singleton
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
class TunnelModule { class TunnelModule {
@Provides @Provides
@Singleton @Singleton
@TunnelShell fun provideRootShell(@ApplicationContext context: Context): RootShell {
fun provideTunnelRootShell(@ApplicationContext context: Context): RootShell {
return RootShell(context)
}
@Provides
@Singleton
@AppShell
fun provideAppRootShell(@ApplicationContext context: Context): RootShell {
return RootShell(context) return RootShell(context)
} }
@@ -49,14 +39,14 @@ class TunnelModule {
@Provides @Provides
@Singleton @Singleton
@Userspace @Userspace
fun provideUserspaceBackend(@ApplicationContext context: Context, @TunnelShell rootShell: RootShell): Backend { fun provideUserspaceBackend(@ApplicationContext context: Context, rootShell: RootShell): Backend {
return GoBackend(context, RootTunnelActionHandler(rootShell)) return GoBackend(context, RootTunnelActionHandler(rootShell))
} }
@Provides @Provides
@Singleton @Singleton
@Kernel @Kernel
fun provideKernelBackend(@ApplicationContext context: Context, @TunnelShell rootShell: RootShell): Backend { fun provideKernelBackend(@ApplicationContext context: Context, rootShell: RootShell): Backend {
return WgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell), RootTunnelActionHandler(rootShell)) return WgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell), RootTunnelActionHandler(rootShell))
} }
@@ -75,7 +65,6 @@ class TunnelModule {
tunnelConfigRepository: TunnelConfigRepository, tunnelConfigRepository: TunnelConfigRepository,
@ApplicationScope applicationScope: CoroutineScope, @ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher, @IoDispatcher ioDispatcher: CoroutineDispatcher,
serviceManager: ServiceManager,
): TunnelService { ): TunnelService {
return WireGuardTunnel( return WireGuardTunnel(
amneziaBackend, amneziaBackend,
@@ -84,13 +73,6 @@ class TunnelModule {
appDataRepository, appDataRepository,
applicationScope, applicationScope,
ioDispatcher, ioDispatcher,
serviceManager,
) )
} }
@Singleton
@Provides
fun provideServiceManager(@ApplicationContext context: Context): ServiceManager {
return ServiceManager.getInstance(context)
}
} }
@@ -7,12 +7,12 @@ import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider
@AndroidEntryPoint @AndroidEntryPoint
class AppUpdateReceiver : BroadcastReceiver() { class AppUpdateReceiver : BroadcastReceiver() {
@@ -25,10 +25,7 @@ class AppUpdateReceiver : BroadcastReceiver() {
lateinit var appDataRepository: AppDataRepository lateinit var appDataRepository: AppDataRepository
@Inject @Inject
lateinit var tunnelService: Provider<TunnelService> lateinit var tunnelService: TunnelService
@Inject
lateinit var serviceManager: ServiceManager
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
if (intent.action != Intent.ACTION_MY_PACKAGE_REPLACED) return if (intent.action != Intent.ACTION_MY_PACKAGE_REPLACED) return
@@ -36,11 +33,11 @@ class AppUpdateReceiver : BroadcastReceiver() {
val settings = appDataRepository.settings.getSettings() val settings = appDataRepository.settings.getSettings()
if (settings.isAutoTunnelEnabled) { if (settings.isAutoTunnelEnabled) {
Timber.i("Restarting services after upgrade") Timber.i("Restarting services after upgrade")
serviceManager.startAutoTunnel(true) ServiceManager.startWatcherServiceForeground(context)
} }
if (!settings.isAutoTunnelEnabled) { if (!settings.isAutoTunnelEnabled || settings.isAutoTunnelPaused) {
val tunnels = appDataRepository.tunnels.getAll().filter { it.isActive } val tunnels = appDataRepository.tunnels.getAll().filter { it.isActive }
if (tunnels.isNotEmpty()) tunnelService.get().startTunnel(tunnels.first(), true) if (tunnels.isNotEmpty()) context.startTunnelBackground(tunnels.first().id)
} }
} }
} }
@@ -0,0 +1,61 @@
package com.zaneschepke.wireguardautotunnel.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Provider
@AndroidEntryPoint
class BackgroundActionReceiver : BroadcastReceiver() {
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@Inject
lateinit var tunnelService: Provider<TunnelService>
@Inject
lateinit var tunnelConfigRepository: TunnelConfigRepository
override fun onReceive(context: Context, intent: Intent) {
val id = intent.getIntExtra(TUNNEL_ID_EXTRA_KEY, 0)
if (id == 0) return
when (intent.action) {
ACTION_CONNECT -> {
Timber.d("Connect actions")
applicationScope.launch {
val tunnel = tunnelConfigRepository.getById(id)
tunnel?.let {
ServiceManager.startTunnelBackgroundService(context)
tunnelService.get().startTunnel(it)
}
}
}
ACTION_DISCONNECT -> {
applicationScope.launch {
val tunnel = tunnelConfigRepository.getById(id)
tunnel?.let {
ServiceManager.stopTunnelBackgroundService(context)
tunnelService.get().stopTunnel(it)
}
}
}
}
}
companion object {
const val ACTION_CONNECT = "ACTION_CONNECT"
const val ACTION_DISCONNECT = "ACTION_DISCONNECT"
const val TUNNEL_ID_EXTRA_KEY = "tunnelId"
}
}
@@ -8,6 +8,7 @@ import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -27,9 +28,6 @@ class BootReceiver : BroadcastReceiver() {
@ApplicationScope @ApplicationScope
lateinit var applicationScope: CoroutineScope lateinit var applicationScope: CoroutineScope
@Inject
lateinit var serviceManager: ServiceManager
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
if (Intent.ACTION_BOOT_COMPLETED != intent.action) return if (Intent.ACTION_BOOT_COMPLETED != intent.action) return
applicationScope.launch { applicationScope.launch {
@@ -39,11 +37,11 @@ class BootReceiver : BroadcastReceiver() {
val tunState = tunnelService.get().vpnState.value.status val tunState = tunnelService.get().vpnState.value.status
if (activeTunnels.isNotEmpty() && tunState != TunnelState.UP) { if (activeTunnels.isNotEmpty() && tunState != TunnelState.UP) {
Timber.i("Starting previously active tunnel") Timber.i("Starting previously active tunnel")
tunnelService.get().startTunnel(activeTunnels.first(), true) context.startTunnelBackground(activeTunnels.first().id)
} }
if (isAutoTunnelEnabled) { if (isAutoTunnelEnabled) {
Timber.i("Starting watcher service from boot") Timber.i("Starting watcher service from boot")
serviceManager.startAutoTunnel(true) ServiceManager.startWatcherServiceForeground(context)
} }
} }
} }
@@ -1,5 +1,6 @@
package com.zaneschepke.wireguardautotunnel.service.foreground package com.zaneschepke.wireguardautotunnel.service.foreground
import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.os.IBinder import android.os.IBinder
@@ -12,7 +13,6 @@ import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.Settings import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.AppShell
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.module.MainImmediateDispatcher import com.zaneschepke.wireguardautotunnel.module.MainImmediateDispatcher
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
@@ -26,14 +26,15 @@ import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.cancelWithMessage import com.zaneschepke.wireguardautotunnel.util.extensions.cancelWithMessage
import com.zaneschepke.wireguardautotunnel.util.extensions.getCurrentWifiName import com.zaneschepke.wireguardautotunnel.util.extensions.getCurrentWifiName
import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList
import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable
import com.zaneschepke.wireguardautotunnel.util.extensions.onNotRunning import com.zaneschepke.wireguardautotunnel.util.extensions.onNotRunning
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
@@ -49,7 +50,6 @@ class AutoTunnelService : LifecycleService() {
private val foregroundId = 122 private val foregroundId = 122
@Inject @Inject
@AppShell
lateinit var rootShell: Provider<RootShell> lateinit var rootShell: Provider<RootShell>
@Inject @Inject
@@ -74,9 +74,6 @@ class AutoTunnelService : LifecycleService() {
@IoDispatcher @IoDispatcher
lateinit var ioDispatcher: CoroutineDispatcher lateinit var ioDispatcher: CoroutineDispatcher
@Inject
lateinit var serviceManager: ServiceManager
@Inject @Inject
@MainImmediateDispatcher @MainImmediateDispatcher
lateinit var mainImmediateDispatcher: CoroutineDispatcher lateinit var mainImmediateDispatcher: CoroutineDispatcher
@@ -91,11 +88,14 @@ class AutoTunnelService : LifecycleService() {
private var pingJob: Job? = null private var pingJob: Job? = null
private var networkEventJob: Job? = null private var networkEventJob: Job? = null
@get:Synchronized @set:Synchronized
private var running: Boolean = false
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
lifecycleScope.launch(mainImmediateDispatcher) { lifecycleScope.launch(mainImmediateDispatcher) {
kotlin.runCatching { kotlin.runCatching {
launchWatcherNotification() launchNotification()
}.onFailure { }.onFailure {
Timber.e(it) Timber.e(it)
} }
@@ -110,14 +110,32 @@ class AutoTunnelService : LifecycleService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Timber.d("onStartCommand executed with startId: $startId") Timber.d("onStartCommand executed with startId: $startId")
serviceManager.autoTunnelService.complete(this) if (intent != null) {
val action = intent.action
when (action) {
Action.START.name,
Action.START_FOREGROUND.name,
-> startService()
Action.STOP.name, Action.STOP_FOREGROUND.name -> stopService()
}
}
return super.onStartCommand(intent, flags, startId) return super.onStartCommand(intent, flags, startId)
} }
fun start() { private suspend fun launchNotification() {
if (appDataRepository.settings.getSettings().isAutoTunnelPaused) {
launchWatcherPausedNotification()
} else {
launchWatcherNotification()
}
}
private fun startService() {
if (running) return
running = true
kotlin.runCatching { kotlin.runCatching {
lifecycleScope.launch(mainImmediateDispatcher) { lifecycleScope.launch(mainImmediateDispatcher) {
launchWatcherNotification() launchNotification()
initWakeLock() initWakeLock()
} }
startSettingsJob() startSettingsJob()
@@ -127,7 +145,7 @@ class AutoTunnelService : LifecycleService() {
} }
} }
fun stop() { private fun stopService() {
wakeLock?.let { wakeLock?.let {
if (it.isHeld) { if (it.isHeld) {
it.release() it.release()
@@ -139,11 +157,10 @@ class AutoTunnelService : LifecycleService() {
override fun onDestroy() { override fun onDestroy() {
cancelAndResetNetworkJobs() cancelAndResetNetworkJobs()
cancelAndResetPingJob() cancelAndResetPingJob()
serviceManager.autoTunnelService = CompletableDeferred()
super.onDestroy() super.onDestroy()
} }
private fun launchWatcherNotification(description: String = getString(R.string.monitoring_state_changes)) { private fun launchWatcherNotification(description: String = getString(R.string.watcher_notification_text_active)) {
val notification = val notification =
notificationService.createNotification( notificationService.createNotification(
channelId = getString(R.string.watcher_channel_id), channelId = getString(R.string.watcher_channel_id),
@@ -159,9 +176,13 @@ class AutoTunnelService : LifecycleService() {
) )
} }
private fun launchWatcherPausedNotification() {
launchWatcherNotification(getString(R.string.watcher_notification_text_paused))
}
private fun initWakeLock() { private fun initWakeLock() {
wakeLock = wakeLock =
(getSystemService(POWER_SERVICE) as PowerManager).run { (getSystemService(Context.POWER_SERVICE) as PowerManager).run {
val tag = this.javaClass.name val tag = this.javaClass.name
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply { newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
try { try {
@@ -209,16 +230,28 @@ class AutoTunnelService : LifecycleService() {
when (status) { when (status) {
is NetworkStatus.Available -> { is NetworkStatus.Available -> {
Timber.i("Gained Mobile data connection") Timber.i("Gained Mobile data connection")
emitMobileDataConnected(true) autoTunnelStateFlow.update {
it.copy(
isMobileDataConnected = true,
)
}
} }
is NetworkStatus.CapabilitiesChanged -> { is NetworkStatus.CapabilitiesChanged -> {
emitMobileDataConnected(true) autoTunnelStateFlow.update {
it.copy(
isMobileDataConnected = true,
)
}
Timber.i("Mobile data capabilities changed") Timber.i("Mobile data capabilities changed")
} }
is NetworkStatus.Unavailable -> { is NetworkStatus.Unavailable -> {
emitMobileDataConnected(false) autoTunnelStateFlow.update {
it.copy(
isMobileDataConnected = false,
)
}
Timber.i("Lost mobile data connection") Timber.i("Lost mobile data connection")
} }
} }
@@ -232,7 +265,8 @@ class AutoTunnelService : LifecycleService() {
runCatching { runCatching {
do { do {
val vpnState = tunnelService.get().vpnState.value val vpnState = tunnelService.get().vpnState.value
if (vpnState.status == TunnelState.UP) { val settings = appDataRepository.settings.getSettings()
if (vpnState.status == TunnelState.UP && !settings.isAutoTunnelPaused) {
if (vpnState.tunnelConfig != null) { if (vpnState.tunnelConfig != null) {
val config = TunnelConfig.configFromWgQuick(vpnState.tunnelConfig.wgQuick) val config = TunnelConfig.configFromWgQuick(vpnState.tunnelConfig.wgQuick)
val results = if (vpnState.tunnelConfig.pingIp != null) { val results = if (vpnState.tunnelConfig.pingIp != null) {
@@ -262,6 +296,17 @@ class AutoTunnelService : LifecycleService() {
} }
} }
private fun onAutoTunnelPause(paused: Boolean) {
if (autoTunnelStateFlow.value.settings.isAutoTunnelPaused
!= paused
) {
when (paused) {
true -> launchWatcherPausedNotification()
false -> launchWatcherNotification()
}
}
}
private suspend fun watchForSettingsChanges() { private suspend fun watchForSettingsChanges() {
Timber.i("Starting settings watcher") Timber.i("Starting settings watcher")
withContext(ioDispatcher) { withContext(ioDispatcher) {
@@ -271,13 +316,12 @@ class AutoTunnelService : LifecycleService() {
old.map { it.isActive } != new.map { it.isActive } old.map { it.isActive } != new.map { it.isActive }
}, },
) { settings, tunnels -> ) { settings, tunnels ->
Timber.d("Tunnels or settings changed!")
autoTunnelStateFlow.value.copy( autoTunnelStateFlow.value.copy(
settings = settings, settings = settings,
tunnels = tunnels, tunnels = tunnels,
) )
}.collect { }.collect {
Timber.d("got new settings: ${it.settings}") onAutoTunnelPause(it.settings.isAutoTunnelPaused)
manageJobsBySettings(it.settings) manageJobsBySettings(it.settings)
autoTunnelStateFlow.emit(it) autoTunnelStateFlow.emit(it)
} }
@@ -287,12 +331,7 @@ class AutoTunnelService : LifecycleService() {
private suspend fun watchForVpnStateChanges() { private suspend fun watchForVpnStateChanges() {
Timber.i("Starting vpn state watcher") Timber.i("Starting vpn state watcher")
withContext(ioDispatcher) { withContext(ioDispatcher) {
tunnelService.get().vpnState.distinctUntilChanged { old, new -> tunnelService.get().vpnState.collect { state ->
old.tunnelConfig?.id == new.tunnelConfig?.id
}.collect { state ->
autoTunnelStateFlow.update {
it.copy(vpnState = state)
}
state.tunnelConfig?.let { state.tunnelConfig?.let {
val settings = appDataRepository.settings.getSettings() val settings = appDataRepository.settings.getSettings()
if (it.isPingEnabled && !settings.isPingEnabled) { if (it.isPingEnabled && !settings.isPingEnabled) {
@@ -356,7 +395,7 @@ class AutoTunnelService : LifecycleService() {
mobileDataJob = null mobileDataJob = null
} }
private fun emitEthernetConnected(connected: Boolean) { private fun updateEthernet(connected: Boolean) {
autoTunnelStateFlow.update { autoTunnelStateFlow.update {
it.copy( it.copy(
isEthernetConnected = connected, isEthernetConnected = connected,
@@ -364,7 +403,7 @@ class AutoTunnelService : LifecycleService() {
} }
} }
private fun emitWifiConnected(connected: Boolean) { private fun updateWifi(connected: Boolean) {
autoTunnelStateFlow.update { autoTunnelStateFlow.update {
it.copy( it.copy(
isWifiConnected = connected, isWifiConnected = connected,
@@ -372,22 +411,6 @@ class AutoTunnelService : LifecycleService() {
} }
} }
private fun emitWifiSSID(ssid: String) {
autoTunnelStateFlow.update {
it.copy(
currentNetworkSSID = ssid,
)
}
}
private fun emitMobileDataConnected(connected: Boolean) {
autoTunnelStateFlow.update {
it.copy(
isMobileDataConnected = connected,
)
}
}
private suspend fun watchForEthernetConnectivityChanges() { private suspend fun watchForEthernetConnectivityChanges() {
withContext(ioDispatcher) { withContext(ioDispatcher) {
Timber.i("Starting ethernet data watcher") Timber.i("Starting ethernet data watcher")
@@ -395,16 +418,16 @@ class AutoTunnelService : LifecycleService() {
when (status) { when (status) {
is NetworkStatus.Available -> { is NetworkStatus.Available -> {
Timber.i("Gained Ethernet connection") Timber.i("Gained Ethernet connection")
emitEthernetConnected(true) updateEthernet(true)
} }
is NetworkStatus.CapabilitiesChanged -> { is NetworkStatus.CapabilitiesChanged -> {
Timber.i("Ethernet capabilities changed") Timber.i("Ethernet capabilities changed")
emitEthernetConnected(true) updateEthernet(true)
} }
is NetworkStatus.Unavailable -> { is NetworkStatus.Unavailable -> {
emitEthernetConnected(false) updateEthernet(false)
Timber.i("Lost Ethernet connection") Timber.i("Lost Ethernet connection")
} }
} }
@@ -419,12 +442,12 @@ class AutoTunnelService : LifecycleService() {
when (status) { when (status) {
is NetworkStatus.Available -> { is NetworkStatus.Available -> {
Timber.i("Gained Wi-Fi connection") Timber.i("Gained Wi-Fi connection")
emitWifiConnected(true) updateWifi(true)
} }
is NetworkStatus.CapabilitiesChanged -> { is NetworkStatus.CapabilitiesChanged -> {
Timber.i("Wifi capabilities changed") Timber.i("Wifi capabilities changed")
emitWifiConnected(true) updateWifi(true)
val ssid = getWifiSSID(status.networkCapabilities) val ssid = getWifiSSID(status.networkCapabilities)
ssid?.let { name -> ssid?.let { name ->
if (name.contains(Constants.UNREADABLE_SSID)) { if (name.contains(Constants.UNREADABLE_SSID)) {
@@ -433,12 +456,16 @@ class AutoTunnelService : LifecycleService() {
Timber.i("Detected valid SSID") Timber.i("Detected valid SSID")
} }
appDataRepository.appState.setCurrentSsid(name) appDataRepository.appState.setCurrentSsid(name)
emitWifiSSID(name) autoTunnelStateFlow.update {
it.copy(
currentNetworkSSID = name,
)
}
} ?: Timber.w("Failed to read ssid") } ?: Timber.w("Failed to read ssid")
} }
is NetworkStatus.Unavailable -> { is NetworkStatus.Unavailable -> {
emitWifiConnected(false) updateWifi(false)
Timber.i("Lost Wi-Fi connection") Timber.i("Lost Wi-Fi connection")
} }
} }
@@ -448,8 +475,9 @@ class AutoTunnelService : LifecycleService() {
private suspend fun getWifiSSID(networkCapabilities: NetworkCapabilities): String? { private suspend fun getWifiSSID(networkCapabilities: NetworkCapabilities): String? {
return withContext(ioDispatcher) { return withContext(ioDispatcher) {
with(autoTunnelStateFlow.value.settings) { try {
if (isWifiNameByShellEnabled) return@withContext rootShell.get().getCurrentWifiName() rootShell.get().getCurrentWifiName()
} catch (_: Exception) {
wifiService.getNetworkName(networkCapabilities) wifiService.getNetworkName(networkCapabilities)
} }
} }
@@ -459,95 +487,100 @@ class AutoTunnelService : LifecycleService() {
return appDataRepository.tunnels.findByMobileDataTunnel().firstOrNull() return appDataRepository.tunnels.findByMobileDataTunnel().firstOrNull()
} }
private fun isTunnelDown(): Boolean {
return tunnelService.get().vpnState.value.status == TunnelState.DOWN
}
private suspend fun handleNetworkEventChanges() { private suspend fun handleNetworkEventChanges() {
withContext(ioDispatcher) { withContext(ioDispatcher) {
Timber.i("Starting network event watcher") Timber.i("Starting network event watcher")
autoTunnelStateFlow.collect { watcherState -> autoTunnelStateFlow.collectLatest { watcherState ->
val autoTunnel = "Auto-tunnel watcher" val autoTunnel = "Auto-tunnel watcher"
// delay for rapid network state changes and then collect latest if (!watcherState.settings.isAutoTunnelPaused) {
delay(Constants.WATCHER_COLLECTION_DELAY) // delay for rapid network state changes and then collect latest
val activeTunnel = watcherState.vpnState.tunnelConfig delay(Constants.WATCHER_COLLECTION_DELAY)
val defaultTunnel = appDataRepository.getPrimaryOrFirstTunnel() val activeTunnel = tunnelService.get().vpnState.value.tunnelConfig
val isTunnelDown = tunnelService.get().getState() == TunnelState.DOWN val defaultTunnel = appDataRepository.getPrimaryOrFirstTunnel()
when { when {
watcherState.isEthernetConditionMet() -> { watcherState.isEthernetConditionMet() -> {
Timber.i("$autoTunnel - tunnel on on ethernet condition met") Timber.i("$autoTunnel - tunnel on on ethernet condition met")
if (isTunnelDown) { if (isTunnelDown()) {
defaultTunnel?.let { defaultTunnel?.let {
tunnelService.get().startTunnel(it)
}
}
}
watcherState.isMobileDataConditionMet() -> {
Timber.i("$autoTunnel - tunnel on mobile data condition met")
val mobileDataTunnel = getMobileDataTunnel()
val tunnel =
mobileDataTunnel ?: defaultTunnel
if (isTunnelDown || activeTunnel?.isMobileDataTunnel == false) {
tunnel?.let {
tunnelService.get().startTunnel(it)
}
}
}
watcherState.isTunnelOffOnMobileDataConditionMet() -> {
Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off")
if (!isTunnelDown) {
activeTunnel?.let {
tunnelService.get().stopTunnel(it)
}
}
}
watcherState.isUntrustedWifiConditionMet() -> {
Timber.i("Untrusted wifi condition met")
if (activeTunnel == null || watcherState.isCurrentSSIDActiveTunnelNetwork() == false ||
isTunnelDown
) {
Timber.i(
"$autoTunnel - tunnel on ssid not associated with current tunnel condition met",
)
watcherState.getTunnelWithMatchingTunnelNetwork()?.let {
Timber.i("Found tunnel associated with this SSID, bringing tunnel up: ${it.name}")
if (isTunnelDown || activeTunnel?.id != it.id) {
tunnelService.get().startTunnel(it) tunnelService.get().startTunnel(it)
} }
} ?: suspend { }
Timber.i("No tunnel associated with this SSID, using defaults") }
val default = appDataRepository.getPrimaryOrFirstTunnel()
if (default?.name != tunnelService.get().name || isTunnelDown) { watcherState.isMobileDataConditionMet() -> {
default?.let { Timber.i("$autoTunnel - tunnel on mobile data condition met")
val mobileDataTunnel = getMobileDataTunnel()
val tunnel =
mobileDataTunnel ?: defaultTunnel
if (isTunnelDown() || activeTunnel?.isMobileDataTunnel == false) {
tunnel?.let {
tunnelService.get().startTunnel(it)
}
}
}
watcherState.isTunnelOffOnMobileDataConditionMet() -> {
Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off")
if (!isTunnelDown()) {
activeTunnel?.let {
tunnelService.get().stopTunnel(it)
}
}
}
watcherState.isUntrustedWifiConditionMet() -> {
Timber.i("Untrusted wifi condition met")
if (activeTunnel?.tunnelNetworks?.isMatchingToWildcardList(watcherState.currentNetworkSSID) == false ||
activeTunnel == null || isTunnelDown()
) {
Timber.i(
"$autoTunnel - tunnel on ssid not associated with current tunnel condition met",
)
watcherState.tunnels.firstOrNull { it.tunnelNetworks.isMatchingToWildcardList(watcherState.currentNetworkSSID) }?.let {
Timber.i("Found tunnel associated with this SSID, bringing tunnel up: ${it.name}")
if (isTunnelDown() || activeTunnel?.id != it.id) {
tunnelService.get().startTunnel(it) tunnelService.get().startTunnel(it)
} }
} } ?: suspend {
}.invoke() Timber.i("No tunnel associated with this SSID, using defaults")
val default = appDataRepository.getPrimaryOrFirstTunnel()
if (default?.name != tunnelService.get().name || isTunnelDown()) {
default?.let {
tunnelService.get().startTunnel(it)
}
}
}.invoke()
}
} }
}
watcherState.isTrustedWifiConditionMet() -> { watcherState.isTrustedWifiConditionMet() -> {
Timber.i( Timber.i(
"$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off", "$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off",
) )
if (!isTunnelDown) activeTunnel?.let { tunnelService.get().stopTunnel(it) } if (!isTunnelDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
} }
watcherState.isTunnelOffOnWifiConditionMet() -> { watcherState.isTunnelOffOnWifiConditionMet() -> {
Timber.i( Timber.i(
"$autoTunnel - tunnel off on wifi condition met, turning vpn off", "$autoTunnel - tunnel off on wifi condition met, turning vpn off",
) )
if (!isTunnelDown) activeTunnel?.let { tunnelService.get().stopTunnel(it) } if (!isTunnelDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
} }
// TODO disable for this now
// watcherState.isTunnelOffOnNoConnectivityMet() -> {
// Timber.i(
// "$autoTunnel - tunnel off on no connectivity met, turning vpn off",
// )
// if (!isTunnelDown) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
// }
else -> { watcherState.isTunnelOffOnNoConnectivityMet() -> {
Timber.i("$autoTunnel - no condition met") Timber.i(
"$autoTunnel - tunnel off on no connectivity met, turning vpn off",
)
if (!isTunnelDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
}
else -> {
Timber.i("$autoTunnel - no condition met")
}
} }
} }
} }
@@ -1,13 +1,10 @@
package com.zaneschepke.wireguardautotunnel.service.foreground package com.zaneschepke.wireguardautotunnel.service.foreground
import com.zaneschepke.wireguardautotunnel.data.domain.Settings import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList
data class AutoTunnelState( data class AutoTunnelState(
val vpnState: VpnState = VpnState(),
val isWifiConnected: Boolean = false, val isWifiConnected: Boolean = false,
val isEthernetConnected: Boolean = false, val isEthernetConnected: Boolean = false,
val isMobileDataConnected: Boolean = false, val isMobileDataConnected: Boolean = false,
@@ -44,7 +41,7 @@ data class AutoTunnelState(
return ( return (
!isEthernetConnected && !isEthernetConnected &&
isWifiConnected && isWifiConnected &&
!isCurrentSSIDTrusted() && !settings.trustedNetworkSSIDs.isMatchingToWildcardList(currentNetworkSSID) &&
settings.isTunnelOnWifiEnabled settings.isTunnelOnWifiEnabled
) )
} }
@@ -54,7 +51,7 @@ data class AutoTunnelState(
!isEthernetConnected && !isEthernetConnected &&
( (
isWifiConnected && isWifiConnected &&
isCurrentSSIDTrusted() settings.trustedNetworkSSIDs.isMatchingToWildcardList(currentNetworkSSID)
) )
) )
} }
@@ -76,32 +73,4 @@ data class AutoTunnelState(
!isMobileDataConnected !isMobileDataConnected
) )
} }
fun isCurrentSSIDTrusted(): Boolean {
return if (settings.isWildcardsEnabled) {
settings.trustedNetworkSSIDs.isMatchingToWildcardList(currentNetworkSSID)
} else {
settings.trustedNetworkSSIDs.contains(currentNetworkSSID)
}
}
fun isCurrentSSIDActiveTunnelNetwork(): Boolean {
val currentTunnelNetworks = vpnState.tunnelConfig?.tunnelNetworks
return (
if (settings.isWildcardsEnabled) {
currentTunnelNetworks?.isMatchingToWildcardList(currentNetworkSSID)
} else {
currentTunnelNetworks?.contains(currentNetworkSSID)
}
) == true
}
fun getTunnelWithMatchingTunnelNetwork(): TunnelConfig? {
return tunnels.firstOrNull {
if (settings.isWildcardsEnabled) {
it.tunnelNetworks.isMatchingToWildcardList(currentNetworkSSID)
} else {
it.tunnelNetworks.contains(currentNetworkSSID)
}
}
}
} }
@@ -3,83 +3,69 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.Service import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import com.zaneschepke.wireguardautotunnel.util.SingletonHolder import android.net.VpnService
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import jakarta.inject.Inject
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import timber.log.Timber import timber.log.Timber
@OptIn(ExperimentalCoroutinesApi::class) object ServiceManager {
class ServiceManager private fun <T : Service> actionOnService(action: Action, context: Context, cls: Class<T>, extras: Map<String, Int>? = null) {
@Inject constructor(private val context: Context) { if (VpnService.prepare(context) != null) return
val intent =
private val _autoTunnelActive = MutableStateFlow(false) Intent(context, cls).also {
it.action = action.name
val autoTunnelActive = _autoTunnelActive.asStateFlow() extras?.forEach { (k, v) -> it.putExtra(k, v) }
var autoTunnelService = CompletableDeferred<AutoTunnelService>()
var backgroundService = CompletableDeferred<TunnelBackgroundService>()
companion object : SingletonHolder<ServiceManager, Context>(::ServiceManager)
private fun <T : Service> startService(cls: Class<T>, background: Boolean) {
runCatching {
val intent = Intent(context, cls)
if (background) {
context.startForegroundService(intent)
} else {
context.startService(intent)
} }
}.onFailure { Timber.e(it) } intent.component?.javaClass
} try {
when (action) {
Action.START_FOREGROUND, Action.STOP_FOREGROUND ->
context.startForegroundService(
intent,
)
suspend fun startAutoTunnel(background: Boolean) { Action.START, Action.STOP -> context.startService(intent)
if (autoTunnelService.isCompleted) return _autoTunnelActive.update { true } }
kotlin.runCatching { } catch (e: Exception) {
startService(AutoTunnelService::class.java, background) Timber.e(e.message)
autoTunnelService.await()
autoTunnelService.getCompleted().start()
_autoTunnelActive.update { true }
}.onFailure {
Timber.e(it)
} }
} }
suspend fun startBackgroundService() { fun startWatcherServiceForeground(context: Context) {
if (backgroundService.isCompleted) return actionOnService(
kotlin.runCatching { Action.START_FOREGROUND,
startService(TunnelBackgroundService::class.java, true) context,
backgroundService.await() AutoTunnelService::class.java,
backgroundService.getCompleted().start() )
}.onFailure {
Timber.e(it)
}
} }
fun stopBackgroundService() { fun startWatcherService(context: Context) {
if (!backgroundService.isCompleted) return actionOnService(
runCatching { Action.START,
backgroundService.getCompleted().stop() context,
}.onFailure { AutoTunnelService::class.java,
Timber.e(it) )
}
} }
fun stopAutoTunnel() { fun stopWatcherService(context: Context) {
if (!autoTunnelService.isCompleted) return actionOnService(
runCatching { Action.STOP,
autoTunnelService.getCompleted().stop() context,
_autoTunnelActive.update { false } AutoTunnelService::class.java,
}.onFailure { )
Timber.e(it)
}
} }
fun requestTunnelTileUpdate() { fun startTunnelBackgroundService(context: Context) {
context.requestTunnelTileServiceStateUpdate() actionOnService(
Action.START_FOREGROUND,
context,
TunnelBackgroundService::class.java,
)
}
fun stopTunnelBackgroundService(context: Context) {
actionOnService(
Action.STOP,
context,
TunnelBackgroundService::class.java,
)
} }
} }
@@ -3,13 +3,10 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.Notification import android.app.Notification
import android.content.Intent import android.content.Intent
import android.os.IBinder import android.os.IBinder
import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService import androidx.lifecycle.LifecycleService
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.util.Constants
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@@ -18,14 +15,11 @@ class TunnelBackgroundService : LifecycleService() {
@Inject @Inject
lateinit var notificationService: NotificationService lateinit var notificationService: NotificationService
@Inject
lateinit var serviceManager: ServiceManager
private val foregroundId = 123 private val foregroundId = 123
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
start() startForeground(foregroundId, createNotification())
} }
override fun onBind(intent: Intent): IBinder? { override fun onBind(intent: Intent): IBinder? {
@@ -35,34 +29,32 @@ class TunnelBackgroundService : LifecycleService() {
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
serviceManager.backgroundService.complete(this) if (intent != null) {
val action = intent.action
when (action) {
Action.START.name,
Action.START_FOREGROUND.name,
-> startService()
Action.STOP.name, Action.STOP_FOREGROUND.name -> stopService()
}
}
return super.onStartCommand(intent, flags, startId) return super.onStartCommand(intent, flags, startId)
} }
fun start() { private fun startService() {
ServiceCompat.startForeground( startForeground(foregroundId, createNotification())
this,
foregroundId,
createNotification(),
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
)
} }
fun stop() { private fun stopService() {
stopForeground(STOP_FOREGROUND_REMOVE) stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf() stopSelf()
} }
override fun onDestroy() {
serviceManager.backgroundService = CompletableDeferred()
super.onDestroy()
}
private fun createNotification(): Notification { private fun createNotification(): Notification {
return notificationService.createNotification( return notificationService.createNotification(
getString(R.string.vpn_channel_id), getString(R.string.vpn_channel_id),
getString(R.string.vpn_channel_name), getString(R.string.vpn_channel_name),
getString(R.string.tunnel_running), getString(R.string.tunnel_start_text),
description = "", description = "",
) )
} }
@@ -6,8 +6,9 @@ import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.Action import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.AutoTunnelService import com.zaneschepke.wireguardautotunnel.service.foreground.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground
import com.zaneschepke.wireguardautotunnel.util.extensions.stopTunnelBackground
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -23,9 +24,6 @@ class ShortcutsActivity : ComponentActivity() {
@Inject @Inject
lateinit var tunnelService: Provider<TunnelService> lateinit var tunnelService: Provider<TunnelService>
@Inject
lateinit var serviceManager: ServiceManager
@Inject @Inject
@ApplicationScope @ApplicationScope
lateinit var applicationScope: CoroutineScope lateinit var applicationScope: CoroutineScope
@@ -46,16 +44,26 @@ class ShortcutsActivity : ComponentActivity() {
Timber.d("Shortcut action on name: ${tunnelConfig?.name}") Timber.d("Shortcut action on name: ${tunnelConfig?.name}")
tunnelConfig?.let { tunnelConfig?.let {
when (intent.action) { when (intent.action) {
Action.START.name -> tunnelService.get().startTunnel(it, true) Action.START.name -> this@ShortcutsActivity.startTunnelBackground(it.id)
Action.STOP.name -> tunnelService.get().stopTunnel(it) Action.STOP.name -> this@ShortcutsActivity.stopTunnelBackground(it.id)
else -> Unit else -> Unit
} }
} }
} }
AutoTunnelService::class.java.simpleName, LEGACY_AUTO_TUNNEL_SERVICE_NAME -> { AutoTunnelService::class.java.simpleName, LEGACY_AUTO_TUNNEL_SERVICE_NAME -> {
when (intent.action) { when (intent.action) {
Action.START.name -> serviceManager.startAutoTunnel(true) Action.START.name ->
Action.STOP.name -> serviceManager.stopAutoTunnel() appDataRepository.settings.save(
settings.copy(
isAutoTunnelPaused = false,
),
)
Action.STOP.name ->
appDataRepository.settings.save(
settings.copy(
isAutoTunnelPaused = true,
),
)
} }
} }
} }
@@ -1,6 +1,7 @@
package com.zaneschepke.wireguardautotunnel.service.tile package com.zaneschepke.wireguardautotunnel.service.tile
import android.content.Intent import android.content.Intent
import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.service.quicksettings.Tile import android.service.quicksettings.Tile
import android.service.quicksettings.TileService import android.service.quicksettings.TileService
@@ -8,9 +9,9 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -22,18 +23,51 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
@Inject @Inject
lateinit var appDataRepository: AppDataRepository lateinit var appDataRepository: AppDataRepository
@Inject
lateinit var serviceManager: ServiceManager
@Inject @Inject
@ApplicationScope @ApplicationScope
lateinit var applicationScope: CoroutineScope lateinit var applicationScope: CoroutineScope
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this) private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
/* This works around an annoying unsolved frameworks bug some people are hitting. */
override fun onBind(intent: Intent): IBinder? {
var ret: IBinder? = null
try {
ret = super.onBind(intent)
} catch (e: Throwable) {
Timber.e("Failed to bind to AutoTunnelTile")
}
return ret
}
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
applicationScope.launch {
appDataRepository.settings.getSettingsFlow().collect {
kotlin.runCatching {
when (it.isAutoTunnelEnabled) {
true -> {
if (it.isAutoTunnelPaused) {
setInactive()
setTileDescription(this@AutoTunnelControlTile.getString(R.string.paused))
} else {
setActive()
setTileDescription(this@AutoTunnelControlTile.getString(R.string.active))
}
}
false -> {
setTileDescription(this@AutoTunnelControlTile.getString(R.string.disabled))
setUnavailable()
}
}
}.onFailure {
Timber.e(it)
}
}
}
} }
override fun onStopListening() { override fun onStopListening() {
@@ -48,28 +82,26 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
override fun onStartListening() { override fun onStartListening() {
super.onStartListening() super.onStartListening()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START) lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
lifecycleScope.launch {
if (appDataRepository.tunnels.getAll().isEmpty()) return@launch setUnavailable()
updateTileState()
}
}
private fun updateTileState() {
serviceManager.autoTunnelActive.value.let {
if (it) setActive() else setInactive()
}
} }
override fun onClick() { override fun onClick() {
super.onClick() super.onClick()
unlockAndRun { unlockAndRun {
lifecycleScope.launch { lifecycleScope.launch {
if (serviceManager.autoTunnelActive.value) { kotlin.runCatching {
serviceManager.stopAutoTunnel() val settings = appDataRepository.settings.getSettings()
setInactive() if (settings.isAutoTunnelPaused) {
} else { return@launch appDataRepository.settings.save(
serviceManager.startAutoTunnel(true) settings.copy(
setActive() isAutoTunnelPaused = false,
),
)
}
appDataRepository.settings.save(
settings.copy(
isAutoTunnelPaused = true,
),
)
} }
} }
} }
@@ -96,15 +128,16 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
} }
} }
/* This works around an annoying unsolved frameworks bug some people are hitting. */ private fun setTileDescription(description: String) {
override fun onBind(intent: Intent): IBinder? { kotlin.runCatching {
var ret: IBinder? = null if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
try { qsTile.subtitle = description
ret = super.onBind(intent) }
} catch (_: Throwable) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Timber.e("Failed to bind to TunnelControlTile") qsTile.stateDescription = description
}
qsTile.updateTile()
} }
return ret
} }
override val lifecycle: Lifecycle override val lifecycle: Lifecycle
@@ -1,8 +1,6 @@
package com.zaneschepke.wireguardautotunnel.service.tile package com.zaneschepke.wireguardautotunnel.service.tile
import android.content.Intent
import android.os.Build import android.os.Build
import android.os.IBinder
import android.service.quicksettings.Tile import android.service.quicksettings.Tile
import android.service.quicksettings.TileService import android.service.quicksettings.TileService
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
@@ -13,6 +11,8 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground
import com.zaneschepke.wireguardautotunnel.util.extensions.stopTunnelBackground
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -36,6 +36,7 @@ class TunnelControlTile : TileService(), LifecycleOwner {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Timber.d("onCreate for tile service")
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
} }
@@ -51,7 +52,6 @@ class TunnelControlTile : TileService(), LifecycleOwner {
override fun onStartListening() { override fun onStartListening() {
super.onStartListening() super.onStartListening()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START) lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
Timber.d("Updating tile!")
lifecycleScope.launch { lifecycleScope.launch {
if (appDataRepository.tunnels.getAll().isEmpty()) return@launch setUnavailable() if (appDataRepository.tunnels.getAll().isEmpty()) return@launch setUnavailable()
updateTileState() updateTileState()
@@ -68,15 +68,13 @@ class TunnelControlTile : TileService(), LifecycleOwner {
override fun onClick() { override fun onClick() {
super.onClick() super.onClick()
unlockAndRun { unlockAndRun {
Timber.d("Click")
lifecycleScope.launch { lifecycleScope.launch {
val context = this@TunnelControlTile
val lastActive = appDataRepository.getStartTunnelConfig() val lastActive = appDataRepository.getStartTunnelConfig()
lastActive?.let { tunnel -> lastActive?.let { tunnel ->
if (tunnel.isActive) { if (tunnel.isActive) return@launch context.stopTunnelBackground(tunnel.id)
tunnelService.get().stopTunnel(tunnel) context.startTunnelBackground(tunnel.id)
} else {
tunnelService.get().startTunnel(tunnel, true)
}
updateTileState()
} }
} }
} }
@@ -126,17 +124,6 @@ class TunnelControlTile : TileService(), LifecycleOwner {
} }
} }
/* This works around an annoying unsolved frameworks bug some people are hitting. */
override fun onBind(intent: Intent): IBinder? {
var ret: IBinder? = null
try {
ret = super.onBind(intent)
} catch (_: Throwable) {
Timber.e("Failed to bind to TunnelControlTile")
}
return ret
}
override val lifecycle: Lifecycle override val lifecycle: Lifecycle
get() = lifecycleRegistry get() = lifecycleRegistry
} }
@@ -5,7 +5,7 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
interface TunnelService : Tunnel, org.amnezia.awg.backend.Tunnel { interface TunnelService : Tunnel, org.amnezia.awg.backend.Tunnel {
suspend fun startTunnel(tunnelConfig: TunnelConfig, background: Boolean = false): Result<TunnelState> suspend fun startTunnel(tunnelConfig: TunnelConfig): Result<TunnelState>
suspend fun stopTunnel(tunnelConfig: TunnelConfig): Result<TunnelState> suspend fun stopTunnel(tunnelConfig: TunnelConfig): Result<TunnelState>
@@ -18,6 +18,5 @@ interface TunnelService : Tunnel, org.amnezia.awg.backend.Tunnel {
suspend fun getState(): TunnelState suspend fun getState(): TunnelState
fun cancelStatsJob() fun cancelStatsJob()
fun startStatsJob() fun startStatsJob()
} }
@@ -8,7 +8,6 @@ import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepositor
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.module.Kernel import com.zaneschepke.wireguardautotunnel.module.Kernel
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.AmneziaStatistics import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.AmneziaStatistics
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.WireGuardStatistics import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.WireGuardStatistics
@@ -25,7 +24,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.amnezia.awg.backend.Tunnel import org.amnezia.awg.backend.Tunnel
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider import javax.inject.Provider
@@ -38,7 +36,6 @@ constructor(
private val appDataRepository: AppDataRepository, private val appDataRepository: AppDataRepository,
@ApplicationScope private val applicationScope: CoroutineScope, @ApplicationScope private val applicationScope: CoroutineScope,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher, @IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val serviceManager: ServiceManager,
) : TunnelService { ) : TunnelService {
private val _vpnState = MutableStateFlow(VpnState()) private val _vpnState = MutableStateFlow(VpnState())
@@ -49,12 +46,10 @@ constructor(
vpnState.copy( vpnState.copy(
tunnelConfig = tunnels.firstOrNull { it.id == vpnState.tunnelConfig?.id }, tunnelConfig = tunnels.firstOrNull { it.id == vpnState.tunnelConfig?.id },
) )
}.stateIn(applicationScope, SharingStarted.Eagerly, VpnState()) }.stateIn(applicationScope, SharingStarted.Lazily, VpnState())
private var statsJob: Job? = null private var statsJob: Job? = null
private val runningHandle = AtomicBoolean(false)
private suspend fun backend(): Any { private suspend fun backend(): Any {
val settings = appDataRepository.settings.getSettings() val settings = appDataRepository.settings.getSettings()
if (settings.isKernelEnabled) return kernelBackend.get() if (settings.isKernelEnabled) return kernelBackend.get()
@@ -92,18 +87,11 @@ constructor(
} }
} }
override suspend fun startTunnel(tunnelConfig: TunnelConfig, background: Boolean): Result<TunnelState> { override suspend fun startTunnel(tunnelConfig: TunnelConfig): Result<TunnelState> {
return withContext(ioDispatcher) { return withContext(ioDispatcher) {
if (runningHandle.get() && tunnelConfig == vpnState.value.tunnelConfig) {
Timber.w("Tunnel already running")
return@withContext Result.success(vpnState.value.status)
}
runningHandle.set(true)
onBeforeStart(tunnelConfig) onBeforeStart(tunnelConfig)
val settings = appDataRepository.settings.getSettings()
if (background || settings.isKernelEnabled) startBackgroundService()
setState(tunnelConfig, TunnelState.UP).onSuccess { setState(tunnelConfig, TunnelState.UP).onSuccess {
updateTunnelState(it) emitTunnelState(it)
}.onFailure { }.onFailure {
Timber.e(it) Timber.e(it)
onStartFailed() onStartFailed()
@@ -115,13 +103,10 @@ constructor(
return withContext(ioDispatcher) { return withContext(ioDispatcher) {
onBeforeStop(tunnelConfig) onBeforeStop(tunnelConfig)
setState(tunnelConfig, TunnelState.DOWN).onSuccess { setState(tunnelConfig, TunnelState.DOWN).onSuccess {
updateTunnelState(it) emitTunnelState(it)
}.onFailure { }.onFailure {
Timber.e(it) Timber.e(it)
onStopFailed() onStopFailed()
}.also {
stopBackgroundService()
runningHandle.set(false)
} }
} }
} }
@@ -136,7 +121,7 @@ constructor(
private suspend fun toggleTunnel(tunnelConfig: TunnelConfig): Result<TunnelState> { private suspend fun toggleTunnel(tunnelConfig: TunnelConfig): Result<TunnelState> {
return withContext(ioDispatcher) { return withContext(ioDispatcher) {
setState(tunnelConfig, TunnelState.TOGGLE).onSuccess { setState(tunnelConfig, TunnelState.TOGGLE).onSuccess {
updateTunnelState(it) emitTunnelState(it)
resetBackendStatistics() resetBackendStatistics()
}.onFailure { }.onFailure {
Timber.e(it) Timber.e(it)
@@ -156,48 +141,28 @@ constructor(
} }
cancelStatsJob() cancelStatsJob()
resetBackendStatistics() resetBackendStatistics()
runningHandle.set(false)
}
private suspend fun shutDownActiveTunnel(config: TunnelConfig) {
with(_vpnState.value) {
if (status == TunnelState.UP && tunnelConfig != config) {
tunnelConfig?.let { stopTunnel(it) }
}
}
}
private suspend fun startBackgroundService() {
serviceManager.startBackgroundService()
serviceManager.requestTunnelTileUpdate()
}
private fun stopBackgroundService() {
serviceManager.stopBackgroundService()
serviceManager.requestTunnelTileUpdate()
} }
private suspend fun onBeforeStart(tunnelConfig: TunnelConfig) { private suspend fun onBeforeStart(tunnelConfig: TunnelConfig) {
shutDownActiveTunnel(tunnelConfig) if (_vpnState.value.status == TunnelState.UP) vpnState.value.tunnelConfig?.let { stopTunnel(it) }
resetBackendStatistics()
appDataRepository.tunnels.save(tunnelConfig.copy(isActive = true)) appDataRepository.tunnels.save(tunnelConfig.copy(isActive = true))
emitVpnStateConfig(tunnelConfig) emitVpnStateConfig(tunnelConfig)
resetBackendStatistics()
startStatsJob() startStatsJob()
} }
private suspend fun onBeforeStop(tunnelConfig: TunnelConfig) { private suspend fun onBeforeStop(tunnelConfig: TunnelConfig) {
appDataRepository.tunnels.save(tunnelConfig.copy(isActive = false))
cancelStatsJob() cancelStatsJob()
resetBackendStatistics() resetBackendStatistics()
appDataRepository.tunnels.save(tunnelConfig.copy(isActive = false))
} }
private fun updateTunnelState(state: TunnelState) { private fun emitTunnelState(state: TunnelState) {
_vpnState.tryEmit( _vpnState.tryEmit(
_vpnState.value.copy( _vpnState.value.copy(
status = state, status = state,
), ),
) )
serviceManager.requestTunnelTileUpdate()
} }
private fun emitBackendStatistics(statistics: TunnelStatistics) { private fun emitBackendStatistics(statistics: TunnelStatistics) {
@@ -265,15 +230,15 @@ constructor(
} }
override fun onStateChange(newState: Tunnel.State) { override fun onStateChange(newState: Tunnel.State) {
updateTunnelState(TunnelState.from(newState)) emitTunnelState(TunnelState.from(newState))
} }
override fun onStateChange(state: State) { override fun onStateChange(state: State) {
updateTunnelState(TunnelState.from(state)) emitTunnelState(TunnelState.from(state))
} }
companion object { companion object {
const val STATS_START_DELAY = 1_000L const val STATS_START_DELAY = 5_000L
const val VPN_STATISTIC_CHECK_INTERVAL = 1_000L const val VPN_STATISTIC_CHECK_INTERVAL = 1_000L
const val VPN_RESTART_DELAY = 1_000L const val VPN_RESTART_DELAY = 1_000L
} }
@@ -10,5 +10,4 @@ data class AppUiState(
val tunnels: List<TunnelConfig> = emptyList(), val tunnels: List<TunnelConfig> = emptyList(),
val vpnState: VpnState = VpnState(), val vpnState: VpnState = VpnState(),
val generalState: GeneralState = GeneralState(), val generalState: GeneralState = GeneralState(),
val autoTunnelActive: Boolean = false,
) )
@@ -1,25 +1,15 @@
package com.zaneschepke.wireguardautotunnel.ui package com.zaneschepke.wireguardautotunnel.ui
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.util.RootShell
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.AppShell
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil.OPTION_PHONE_LANGUAGE
import com.zaneschepke.wireguardautotunnel.util.StringValue
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -30,10 +20,8 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import kotlinx.coroutines.withContext
import xyz.teamgravity.pin_lock_compose.PinManager import xyz.teamgravity.pin_lock_compose.PinManager
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider import javax.inject.Provider
@@ -45,39 +33,34 @@ constructor(
private val appDataRepository: AppDataRepository, private val appDataRepository: AppDataRepository,
private val tunnelService: Provider<TunnelService>, private val tunnelService: Provider<TunnelService>,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher, @IoDispatcher private val ioDispatcher: CoroutineDispatcher,
@AppShell private val rootShell: Provider<RootShell>,
private val serviceManager: ServiceManager,
private val logReader: LogReader,
) : ViewModel() { ) : ViewModel() {
private val _appUiState = MutableStateFlow(AppUiState())
val uiState = val uiState =
combine( combine(
appDataRepository.settings.getSettingsFlow(), appDataRepository.settings.getSettingsFlow(),
appDataRepository.tunnels.getTunnelConfigsFlow(), appDataRepository.tunnels.getTunnelConfigsFlow(),
tunnelService.get().vpnState, tunnelService.get().vpnState,
appDataRepository.appState.generalStateFlow, appDataRepository.appState.generalStateFlow,
serviceManager.autoTunnelActive, ) { settings, tunnels, tunnelState, generalState ->
) { settings, tunnels, tunnelState, generalState, autoTunnel ->
AppUiState( AppUiState(
settings, settings,
tunnels, tunnels,
tunnelState, tunnelState,
generalState, generalState,
autoTunnel,
) )
}.stateIn( }.stateIn(
viewModelScope + ioDispatcher, viewModelScope + ioDispatcher,
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT), SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
AppUiState(), _appUiState.value,
) )
private val _isAppReady = MutableStateFlow(false) private val _isAppReady = MutableStateFlow<Boolean>(false)
val isAppReady = _isAppReady.asStateFlow() val isAppReady = _isAppReady.asStateFlow()
private val _configurationChange = MutableStateFlow(false)
val configurationChange = _configurationChange.asStateFlow()
init { init {
viewModelScope.launch { viewModelScope.launch {
initPin() initPin()
initAutoTunnel() initAutoTunnel()
@@ -110,7 +93,15 @@ constructor(
private suspend fun initAutoTunnel() { private suspend fun initAutoTunnel() {
val settings = appDataRepository.settings.getSettings() val settings = appDataRepository.settings.getSettings()
if (settings.isAutoTunnelEnabled) serviceManager.startAutoTunnel(false) if (settings.isAutoTunnelEnabled) ServiceManager.startWatcherService(WireGuardAutoTunnel.instance)
}
fun setTunnels(tunnels: TunnelConfigs) = viewModelScope.launch(ioDispatcher) {
_appUiState.emit(
_appUiState.value.copy(
tunnels = tunnels,
),
)
} }
fun onPinLockDisabled() = viewModelScope.launch(ioDispatcher) { fun onPinLockDisabled() = viewModelScope.launch(ioDispatcher) {
@@ -121,107 +112,4 @@ constructor(
fun onPinLockEnabled() = viewModelScope.launch { fun onPinLockEnabled() = viewModelScope.launch {
appDataRepository.appState.setPinLockEnabled(true) appDataRepository.appState.setPinLockEnabled(true)
} }
fun setLocationDisclosureShown() = viewModelScope.launch {
appDataRepository.appState.setLocationDisclosureShown(true)
}
fun onToggleLocalLogging() = viewModelScope.launch(ioDispatcher) {
with(uiState.value.generalState) {
val toggledOn = !isLocalLogsEnabled
appDataRepository.appState.setLocalLogsEnabled(toggledOn)
if (!toggledOn) onLoggerStop()
_configurationChange.update {
true
}
}
}
private suspend fun onLoggerStop() {
logReader.stop()
logReader.deleteAndClearLogs()
}
fun onToggleAlwaysOnVPN() = viewModelScope.launch {
with(uiState.value.settings) {
appDataRepository.settings.save(
copy(
isAlwaysOnVpnEnabled = !isAlwaysOnVpnEnabled,
),
)
}
}
fun onLocaleChange(localeTag: String) = viewModelScope.launch {
val locale = LocaleUtil.getLocaleFromPrefCode(localeTag)
val storageLocale = if (localeTag == OPTION_PHONE_LANGUAGE) OPTION_PHONE_LANGUAGE else locale
appDataRepository.appState.setLocale(storageLocale)
val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(locale)
AppCompatDelegate.setApplicationLocales(appLocale)
}
fun onToggleRestartAtBoot() = viewModelScope.launch {
with(uiState.value.settings) {
appDataRepository.settings.save(
copy(
isRestoreOnBootEnabled = !isRestoreOnBootEnabled,
),
)
}
}
fun onToggleShortcutsEnabled() = viewModelScope.launch {
with(uiState.value.settings) {
appDataRepository.settings.save(
this.copy(
isShortcutsEnabled = !isShortcutsEnabled,
),
)
}
}
private fun saveKernelMode(enabled: Boolean) = viewModelScope.launch {
with(uiState.value.settings) {
appDataRepository.settings.save(
this.copy(
isKernelEnabled = enabled,
),
)
}
}
fun onToggleKernelMode() = viewModelScope.launch {
with(uiState.value.settings) {
if (!isKernelEnabled) {
requestRoot().onSuccess {
if (!isKernelSupported()) return@onSuccess SnackbarController.showMessage(StringValue.StringResource(R.string.kernel_not_supported))
appDataRepository.settings.save(
copy(
isKernelEnabled = true,
isAmneziaEnabled = false,
),
)
}
} else {
saveKernelMode(enabled = false)
}
}
}
private suspend fun isKernelSupported(): Boolean {
return withContext(ioDispatcher) {
WgQuickBackend.hasKernelSupport()
}
}
private suspend fun requestRoot(): Result<Unit> {
return withContext(ioDispatcher) {
kotlin.runCatching {
rootShell.get().start()
SnackbarController.showMessage(StringValue.StringResource(R.string.root_accepted))
}.onFailure {
SnackbarController.showMessage(StringValue.StringResource(R.string.error_root_denied))
}
}
}
} }
@@ -1,15 +1,16 @@
package com.zaneschepke.wireguardautotunnel.ui package com.zaneschepke.wireguardautotunnel.ui
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
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.material.icons.Icons import androidx.compose.material.icons.Icons
@@ -24,22 +25,31 @@ import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute import androidx.navigation.toRoute
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.CustomSnackBar import com.zaneschepke.wireguardautotunnel.ui.common.navigation.isCurrentRoute
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarControllerProvider import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarControllerProvider
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
@@ -47,33 +57,33 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.options.OptionsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.pinlock.PinLockScreen import com.zaneschepke.wireguardautotunnel.ui.screens.pinlock.PinLockScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.scanner.ScannerScreen import com.zaneschepke.wireguardautotunnel.ui.screens.scanner.ScannerScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.AppearanceScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.display.DisplayScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language.LanguageScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.AutoTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.disclosure.LocationDisclosureScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.logs.LogsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.support.logs.LogsScreen
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 com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject import javax.inject.Inject
import kotlin.system.exitProcess
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
@Inject @Inject
lateinit var appStateRepository: AppStateRepository lateinit var appStateRepository: AppStateRepository
@Inject @Inject
lateinit var tunnelService: TunnelService lateinit var tunnelService: TunnelService
private val viewModel by viewModels<AppViewModel>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge(
val viewModel by viewModels<AppViewModel>() navigationBarStyle = SystemBarStyle.auto(
lightScrim = Color.Transparent.toArgb(),
darkScrim = Color.Transparent.toArgb(),
),
)
installSplashScreen().apply { installSplashScreen().apply {
setKeepOnScreenCondition { setKeepOnScreenCondition {
@@ -82,34 +92,30 @@ class MainActivity : AppCompatActivity() {
} }
setContent { setContent {
val appUiState by viewModel.uiState.collectAsStateWithLifecycle() val appUiState by viewModel.uiState.collectAsStateWithLifecycle(lifecycle = this.lifecycle)
val configurationChange by viewModel.configurationChange.collectAsStateWithLifecycle()
val navController = rememberNavController() val navController = rememberNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState()
LaunchedEffect(configurationChange) { LaunchedEffect(appUiState.vpnState.status) {
if (configurationChange) { val context = this@MainActivity
Intent(this@MainActivity, MainActivity::class.java).also { when (appUiState.vpnState.status) {
startActivity(it) TunnelState.DOWN -> ServiceManager.stopTunnelBackgroundService(context)
exitProcess(0) else -> Unit
}
} }
} context.requestTunnelTileServiceStateUpdate()
LaunchedEffect(appUiState.autoTunnelActive) {
requestAutoTunnelTileServiceUpdate()
} }
with(appUiState.settings) { with(appUiState.settings) {
LaunchedEffect(isAutoTunnelEnabled) { LaunchedEffect(isAutoTunnelPaused, isAutoTunnelEnabled) {
this@MainActivity.requestAutoTunnelTileServiceUpdate() this@MainActivity.requestAutoTunnelTileServiceUpdate()
} }
} }
CompositionLocalProvider(LocalNavController provides navController) { CompositionLocalProvider(LocalNavController provides navController) {
SnackbarControllerProvider { host -> SnackbarControllerProvider { host ->
WireguardAutoTunnelTheme(theme = appUiState.generalState.theme) { WireguardAutoTunnelTheme {
val focusRequester = remember { FocusRequester() }
Scaffold( Scaffold(
contentWindowInsets = WindowInsets(0.dp),
snackbarHost = { snackbarHost = {
SnackbarHost(host) { snackbarData: SnackbarData -> SnackbarHost(host) { snackbarData: SnackbarData ->
CustomSnackBar( CustomSnackBar(
@@ -122,6 +128,16 @@ class MainActivity : AppCompatActivity() {
) )
} }
}, },
modifier =
Modifier
.focusable()
.focusProperties {
if (navBackStackEntry?.isCurrentRoute(Route.Lock) == true) {
Unit
} else {
up = focusRequester
}
},
bottomBar = { bottomBar = {
BottomNavBar( BottomNavBar(
navController, navController,
@@ -144,8 +160,8 @@ class MainActivity : AppCompatActivity() {
), ),
) )
}, },
) { ) { padding ->
Box(modifier = Modifier.fillMaxSize().padding(it)) { Box(modifier = Modifier.fillMaxSize().padding(padding)) {
NavHost( NavHost(
navController, navController,
enterTransition = { fadeIn(tween(Constants.TRANSITION_ANIMATION_TIME)) }, enterTransition = { fadeIn(tween(Constants.TRANSITION_ANIMATION_TIME)) },
@@ -154,6 +170,7 @@ class MainActivity : AppCompatActivity() {
) { ) {
composable<Route.Main> { composable<Route.Main> {
MainScreen( MainScreen(
focusRequester = focusRequester,
uiState = appUiState, uiState = appUiState,
) )
} }
@@ -161,27 +178,14 @@ class MainActivity : AppCompatActivity() {
SettingsScreen( SettingsScreen(
appViewModel = viewModel, appViewModel = viewModel,
uiState = appUiState, uiState = appUiState,
focusRequester = focusRequester,
) )
} }
composable<Route.LocationDisclosure> {
LocationDisclosureScreen(viewModel, appUiState)
}
composable<Route.AutoTunnel> {
AutoTunnelScreen(
appUiState,
)
}
composable<Route.Appearance> {
AppearanceScreen()
}
composable<Route.Language> {
LanguageScreen(appUiState, viewModel)
}
composable<Route.Display> {
DisplayScreen(appUiState)
}
composable<Route.Support> { composable<Route.Support> {
SupportScreen(appUiState, viewModel) SupportScreen(
focusRequester = focusRequester,
appUiState = appUiState,
)
} }
composable<Route.Logs> { composable<Route.Logs> {
LogsScreen() LogsScreen()
@@ -189,6 +193,7 @@ class MainActivity : AppCompatActivity() {
composable<Route.Config> { composable<Route.Config> {
val args = it.toRoute<Route.Config>() val args = it.toRoute<Route.Config>()
ConfigScreen( ConfigScreen(
focusRequester = focusRequester,
tunnelId = args.id, tunnelId = args.id,
) )
} }
@@ -196,6 +201,7 @@ class MainActivity : AppCompatActivity() {
val args = it.toRoute<Route.Option>() val args = it.toRoute<Route.Option>()
OptionsScreen( OptionsScreen(
tunnelId = args.id, tunnelId = args.id,
focusRequester = focusRequester,
appUiState = appUiState, appUiState = appUiState,
) )
} }
@@ -215,9 +221,9 @@ class MainActivity : AppCompatActivity() {
} }
} }
} }
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
// save battery by not polling stats while app is closed
tunnelService.cancelStatsJob() tunnelService.cancelStatsJob()
} }
} }
@@ -9,21 +9,6 @@ sealed class Route {
@Serializable @Serializable
data object Settings : Route() data object Settings : Route()
@Serializable
data object AutoTunnel : Route()
@Serializable
data object LocationDisclosure : Route()
@Serializable
data object Appearance : Route()
@Serializable
data object Display : Route()
@Serializable
data object Language : Route()
@Serializable @Serializable
data object Main : Route() data object Main : Route()
@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -18,7 +17,7 @@ fun ClickableIconButton(onClick: () -> Unit, onIconClick: () -> Unit, text: Stri
onClick = onClick, onClick = onClick,
enabled = enabled, enabled = enabled,
) { ) {
Text(text, Modifier.weight(1f, false), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.primary) Text(text, Modifier.weight(1f, false))
Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing)) Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
Icon( Icon(
imageVector = icon, imageVector = icon,
@@ -16,6 +16,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -29,10 +31,12 @@ fun ExpandingRowListItem(
trailing: @Composable () -> Unit, trailing: @Composable () -> Unit,
isExpanded: Boolean, isExpanded: Boolean,
expanded: @Composable () -> Unit = {}, expanded: @Composable () -> Unit = {},
focusRequester: FocusRequester,
) { ) {
Box( Box(
modifier = modifier =
Modifier Modifier
.focusRequester(focusRequester)
.animateContentSize() .animateContentSize()
.clip(RoundedCornerShape(30.dp)) .clip(RoundedCornerShape(30.dp))
.combinedClickable( .combinedClickable(
@@ -1,37 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.common
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@Composable
fun SelectedLabel() {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
stringResource(id = R.string.selected),
modifier =
Modifier.padding(
horizontal = 24.dp.scaledWidth(),
vertical = 16.dp.scaledHeight(),
),
color =
MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.labelSmall,
)
}
}
@@ -1,99 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.common.button
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@androidx.compose.runtime.Composable
fun IconSurfaceButton(title: String, onClick: () -> Unit, selected: Boolean, leadingIcon: ImageVector? = null, description: String? = null) {
val border: BorderStroke? =
if (selected) {
BorderStroke(
1.dp,
MaterialTheme.colorScheme.primary,
)
} else {
null
}
Card(
modifier =
Modifier
.fillMaxWidth()
.height(IntrinsicSize.Min),
shape = RoundedCornerShape(8.dp),
border = border,
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
) {
Box(
modifier = Modifier.clickable { onClick() }
.fillMaxWidth(),
) {
Column(
modifier =
Modifier
.padding(horizontal = 8.dp.scaledWidth(), vertical = 10.dp.scaledHeight())
.padding(end = 16.dp.scaledWidth()).padding(start = 8.dp.scaledWidth())
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.Start,
) {
Row(
verticalAlignment = Alignment.Companion.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp.scaledWidth()),
) {
Row(
horizontalArrangement = Arrangement.spacedBy(
16.dp.scaledWidth(),
),
verticalAlignment = Alignment.Companion.CenterVertically,
modifier = Modifier.padding(vertical = if (description == null) 10.dp.scaledHeight() else 0.dp),
) {
leadingIcon?.let {
Icon(
leadingIcon,
leadingIcon.name,
Modifier.size(iconSize),
if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface,
)
}
Column {
Text(
title,
style = MaterialTheme.typography.titleMedium,
)
description?.let {
Text(
description,
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyMedium,
)
}
}
}
}
}
}
}
}
@@ -1,18 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.common.button
import androidx.compose.material3.Switch
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
@Composable
fun ScaledSwitch(checked: Boolean, onClick: (checked: Boolean) -> Unit, enabled: Boolean = true, modifier: Modifier = Modifier) {
Switch(
checked,
{ onClick(it) },
modifier.scale((52.dp.scaledHeight() / 52.dp)),
enabled = enabled,
)
}
@@ -1,63 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.common.button
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
@Composable
fun SelectionItemButton(
leading: (@Composable () -> Unit)? = null,
buttonText: String,
trailing: (@Composable () -> Unit)? = null,
onClick: () -> Unit,
ripple: Boolean = true,
) {
Card(
modifier =
Modifier.clip(RoundedCornerShape(8.dp))
.clickable(
indication = if (ripple) ripple() else null,
interactionSource = remember { MutableInteractionSource() },
onClick = { onClick() },
)
.height(56.dp.scaledHeight()),
colors =
CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.background,
),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start,
modifier = Modifier.fillMaxSize(),
) {
leading?.let {
it()
}
Text(
buttonText,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
)
trailing?.let {
it()
}
}
}
}
@@ -1,13 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.common.button.surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
data class SelectionItem(
val leadingIcon: ImageVector? = null,
val trailing: (@Composable () -> Unit)? = null,
val title: (@Composable () -> Unit),
val description: (@Composable () -> Unit)? = null,
val onClick: (() -> Unit)? = null,
val height: Int = 64,
)
@@ -1,86 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.common.button.surface
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@Composable
fun SurfaceSelectionGroupButton(items: List<SelectionItem>) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(8.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
) {
items.mapIndexed { index, item ->
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.then(item.onClick?.let { Modifier.clickable { it() } } ?: Modifier)
.fillMaxWidth(),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp.scaledHeight()),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(start = 16.dp.scaledWidth())
.weight(4f, false)
.fillMaxWidth(),
) {
item.leadingIcon?.let { icon ->
Icon(
icon,
icon.name,
modifier = Modifier.size(iconSize),
)
}
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically),
modifier = Modifier
.fillMaxWidth()
.padding(start = if (item.leadingIcon != null) 16.dp.scaledWidth() else 0.dp)
.padding(vertical = if (item.description == null) 16.dp.scaledHeight() else 6.dp.scaledHeight()),
) {
item.title()
item.description?.let {
it()
}
}
}
item.trailing?.let {
Box(
contentAlignment = Alignment.CenterEnd,
modifier = Modifier
.padding(end = 24.dp.scaledWidth(), start = 16.dp.scaledWidth())
.weight(1f),
) {
it()
}
}
}
}
if (index + 1 != items.size) HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
}
}
}
@@ -3,6 +3,7 @@ package com.zaneschepke.wireguardautotunnel.ui.common.config
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -10,19 +11,22 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
@Composable @Composable
fun ConfigurationToggle( fun ConfigurationToggle(
label: String, label: String,
enabled: Boolean = true, enabled: Boolean = true,
checked: Boolean, checked: Boolean,
padding: Dp,
onCheckChanged: (checked: Boolean) -> Unit, onCheckChanged: (checked: Boolean) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Row( Row(
modifier = modifier =
Modifier Modifier
.fillMaxWidth(), .fillMaxWidth()
.padding(padding),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
@@ -3,7 +3,6 @@ package com.zaneschepke.wireguardautotunnel.ui.common.config
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@@ -11,6 +10,7 @@ import androidx.compose.material.icons.outlined.Save
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
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -18,19 +18,21 @@ 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.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.CustomTextField
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@Composable @Composable
fun SubmitConfigurationTextBox( fun SubmitConfigurationTextBox(
value: String?, value: String?,
label: String, label: String,
hint: String, hint: String,
focusRequester: FocusRequester,
isErrorValue: (value: String?) -> Boolean, isErrorValue: (value: String?) -> Boolean,
onSubmit: (value: String) -> Unit, onSubmit: (value: String) -> Unit,
keyboardOptions: KeyboardOptions = KeyboardOptions( keyboardOptions: KeyboardOptions = KeyboardOptions(
@@ -45,22 +47,18 @@ fun SubmitConfigurationTextBox(
var stateValue by remember { mutableStateOf(value ?: "") } var stateValue by remember { mutableStateOf(value ?: "") }
CustomTextField( OutlinedTextField(
isError = isErrorValue(stateValue), isError = isErrorValue(stateValue),
textStyle = MaterialTheme.typography.bodySmall, modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
value = stateValue, value = stateValue,
onValueChange = { stateValue = it },
interactionSource = interactionSource,
label = { Text(label) },
containerColor = MaterialTheme.colorScheme.surface,
placeholder = { Text(hint, style = MaterialTheme.typography.bodySmall) },
modifier =
Modifier
.padding(
top = 5.dp,
bottom = 10.dp,
).fillMaxWidth().padding(end = 16.dp.scaledWidth()),
singleLine = true, singleLine = true,
interactionSource = interactionSource,
onValueChange = { stateValue = it },
label = { Text(label) },
maxLines = 1,
placeholder = { Text(hint) },
keyboardOptions = keyboardOptions, keyboardOptions = keyboardOptions,
keyboardActions = KeyboardActions( keyboardActions = KeyboardActions(
onDone = { onDone = {
@@ -68,17 +66,16 @@ fun SubmitConfigurationTextBox(
keyboardController?.hide() keyboardController?.hide()
}, },
), ),
trailing = { trailingIcon = {
if (!isErrorValue(stateValue) && isFocused) { if (!isErrorValue(stateValue) && isFocused) {
IconButton(onClick = { IconButton(onClick = {
onSubmit(stateValue) onSubmit(stateValue)
keyboardController?.hide() keyboardController?.hide()
focusManager.clearFocus() focusManager.clearFocus()
}) { }) {
val icon = Icons.Outlined.Save
Icon( Icon(
imageVector = icon, imageVector = Icons.Outlined.Save,
contentDescription = icon.name, contentDescription = stringResource(R.string.save_changes),
tint = MaterialTheme.colorScheme.primary, tint = MaterialTheme.colorScheme.primary,
) )
} }
@@ -1,21 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.common.label
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@Composable
fun GroupLabel(title: String) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start,
) {
Text(
title,
style = MaterialTheme.typography.titleMedium,
)
}
}
@@ -1,33 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.common.label
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import com.zaneschepke.wireguardautotunnel.BuildConfig
import com.zaneschepke.wireguardautotunnel.R
@Composable
fun VersionLabel() {
val clipboardManager = LocalClipboardManager.current
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start,
) {
Text(
"${stringResource(R.string.version)}: ${BuildConfig.VERSION_NAME}",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.outline,
modifier = Modifier.clickable {
clipboardManager.setText(AnnotatedString(BuildConfig.VERSION_NAME))
},
)
}
}
@@ -20,15 +20,15 @@ fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavIte
val navBackStackEntry by navController.currentBackStackEntryAsState() val navBackStackEntry by navController.currentBackStackEntryAsState()
showBottomBar = bottomNavItems.any { showBottomBar = bottomNavItems.any {
navBackStackEntry?.isCurrentRoute(it.route::class) == true navBackStackEntry?.isCurrentRoute(it.route) == true
} }
if (showBottomBar) { if (showBottomBar) {
NavigationBar( NavigationBar(
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surface,
) { ) {
bottomNavItems.forEachIndexed { index, item -> bottomNavItems.forEach { item ->
val selected = navBackStackEntry.isCurrentRoute(item.route::class) val selected = navBackStackEntry.isCurrentRoute(item.route)
NavigationBarItem( NavigationBarItem(
selected = selected, selected = selected,
onClick = { onClick = {
@@ -5,11 +5,10 @@ import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavDestination.Companion.hasRoute import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.NavDestination.Companion.hierarchy
import com.zaneschepke.wireguardautotunnel.ui.Route import com.zaneschepke.wireguardautotunnel.ui.Route
import kotlin.reflect.KClass
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
fun <T : Route> NavBackStackEntry?.isCurrentRoute(cls: KClass<T>): Boolean { fun NavBackStackEntry?.isCurrentRoute(route: Route): Boolean {
return this?.destination?.hierarchy?.any { return this?.destination?.hierarchy?.any {
it.hasRoute(route = cls) it.hasRoute(route = route::class)
} == true } == true
} }
@@ -11,21 +11,19 @@ import androidx.compose.runtime.Composable
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun TopNavBar(title: String, trailing: @Composable () -> Unit = {}, showBack: Boolean = true) { fun TopNavBar(title: String, trailing: @Composable () -> Unit = {}) {
val navController = LocalNavController.current val navController = LocalNavController.current
CenterAlignedTopAppBar( CenterAlignedTopAppBar(
title = { title = {
Text(title) Text(title)
}, },
navigationIcon = { navigationIcon = {
if (showBack) { IconButton(onClick = { navController.popBackStack() }) {
IconButton(onClick = { navController.popBackStack() }) { val icon = Icons.AutoMirrored.Outlined.ArrowBack
val icon = Icons.AutoMirrored.Outlined.ArrowBack Icon(
Icon( imageVector = icon,
imageVector = icon, contentDescription = icon.name,
contentDescription = icon.name, )
)
}
} }
}, },
actions = { actions = {
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.ui.common.snackbar package com.zaneschepke.wireguardautotunnel.ui.common.prompt
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.IntrinsicSize
@@ -0,0 +1,27 @@
package com.zaneschepke.wireguardautotunnel.ui.common.screen
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun LoadingScreen() {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
modifier =
Modifier
.fillMaxSize()
.focusable()
.padding(),
) {
Column(modifier = Modifier.padding(120.dp)) { CircularProgressIndicator() }
}
}
@@ -1,109 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.common.textbox
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CustomTextField(
value: String,
modifier: Modifier = Modifier,
textStyle: TextStyle = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurface),
label: @Composable () -> Unit,
containerColor: Color,
onValueChange: (value: String) -> Unit = {},
singleLine: Boolean = false,
placeholder: @Composable (() -> Unit)? = null,
keyboardOptions: KeyboardOptions,
keyboardActions: KeyboardActions,
supportingText: @Composable (() -> Unit)? = null,
leading: @Composable (() -> Unit)? = null,
trailing: @Composable (() -> Unit)? = null,
isError: Boolean = false,
readOnly: Boolean = false,
enabled: Boolean = true,
interactionSource: MutableInteractionSource,
) {
val space = " "
BasicTextField(
value = value,
textStyle = textStyle,
onValueChange = {
onValueChange(it)
},
keyboardActions = keyboardActions,
keyboardOptions = keyboardOptions,
readOnly = readOnly,
cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurface),
modifier = modifier,
interactionSource = interactionSource,
enabled = enabled,
singleLine = singleLine,
) {
OutlinedTextFieldDefaults.DecorationBox(
value = space + value,
innerTextField = {
if (value.isEmpty()) {
if (placeholder != null) {
placeholder()
}
}
it.invoke()
},
contentPadding = OutlinedTextFieldDefaults.contentPadding(top = 0.dp, bottom = 0.dp),
leadingIcon = leading,
trailingIcon = trailing,
singleLine = singleLine,
supportingText = supportingText,
colors = TextFieldDefaults.colors().copy(
disabledLabelColor = MaterialTheme.colorScheme.onSurface,
disabledContainerColor = containerColor,
focusedLabelColor = MaterialTheme.colorScheme.onSurface,
focusedContainerColor = containerColor,
unfocusedContainerColor = containerColor,
focusedTextColor = MaterialTheme.colorScheme.onSurface,
cursorColor = MaterialTheme.colorScheme.onSurface,
),
enabled = enabled,
label = label,
visualTransformation = VisualTransformation.None,
interactionSource = interactionSource,
placeholder = placeholder,
container = {
OutlinedTextFieldDefaults.ContainerBox(
enabled,
isError = isError,
interactionSource,
colors = TextFieldDefaults.colors().copy(
errorContainerColor = containerColor,
disabledLabelColor = MaterialTheme.colorScheme.onSurface,
disabledContainerColor = containerColor,
focusedIndicatorColor = MaterialTheme.colorScheme.onSurface,
focusedLabelColor = MaterialTheme.colorScheme.onSurface,
focusedContainerColor = containerColor,
unfocusedContainerColor = containerColor,
focusedTextColor = MaterialTheme.colorScheme.onSurface,
cursorColor = MaterialTheme.colorScheme.onSurface,
),
shape = RoundedCornerShape(8.dp),
focusedBorderThickness = 0.5.dp,
unfocusedBorderThickness = 0.5.dp,
)
},
)
}
}
@@ -1,5 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.config package com.zaneschepke.wireguardautotunnel.ui.screens.config
import android.annotation.SuppressLint
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
@@ -60,7 +61,6 @@ import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
@@ -68,11 +68,11 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.config.components.Applicat
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable @Composable
fun ConfigScreen(tunnelId: Int) { fun ConfigScreen(tunnelId: Int, focusRequester: FocusRequester) {
val viewModel = hiltViewModel<ConfigViewModel, ConfigViewModel.ConfigViewModelFactory> { factory -> val viewModel = hiltViewModel<ConfigViewModel, ConfigViewModel.ConfigViewModelFactory> { factory ->
factory.create(tunnelId) factory.create(tunnelId)
} }
@@ -90,8 +90,8 @@ fun ConfigScreen(tunnelId: Int) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle() val uiState by viewModel.uiState.collectAsStateWithLifecycle()
var configType by remember { mutableStateOf<ConfigType?>(null) } var configType by remember { mutableStateOf<ConfigType?>(null) }
val derivedConfigType = remember { val derivedConfigType = remember {
derivedStateOf { derivedStateOf<ConfigType> {
configType ?: if (!uiState.isAmneziaEnabled) ConfigType.WIREGUARD else ConfigType.AMNEZIA configType ?: if (!uiState.hasAmneziaProperties()) ConfigType.WIREGUARD else ConfigType.AMNEZIA
} }
} }
val saved by viewModel.saved.collectAsStateWithLifecycle(null) val saved by viewModel.saved.collectAsStateWithLifecycle(null)
@@ -102,6 +102,18 @@ fun ConfigScreen(tunnelId: Int) {
} }
} }
LaunchedEffect(Unit) {
if (!uiState.loading && context.isRunningOnTv()) {
delay(Constants.FOCUS_REQUEST_DELAY)
kotlin.runCatching {
focusRequester.requestFocus()
}.onFailure {
delay(Constants.FOCUS_REQUEST_DELAY)
focusRequester.requestFocus()
}
}
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
delay(2_000L) delay(2_000L)
viewModel.cleanUpUninstalledApps() viewModel.cleanUpUninstalledApps()
@@ -162,9 +174,6 @@ fun ConfigScreen(tunnelId: Int) {
} }
Scaffold( Scaffold(
topBar = {
TopNavBar(stringResource(R.string.edit_tunnel))
},
floatingActionButtonPosition = FabPosition.End, floatingActionButtonPosition = FabPosition.End,
floatingActionButton = { floatingActionButton = {
FloatingActionButton( FloatingActionButton(
@@ -181,8 +190,8 @@ fun ConfigScreen(tunnelId: Int) {
) )
} }
}, },
) { padding -> ) {
Column(Modifier.padding(padding)) { Column {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
@@ -207,7 +216,7 @@ fun ConfigScreen(tunnelId: Int) {
Modifier.fillMaxWidth(fillMaxWidth) Modifier.fillMaxWidth(fillMaxWidth)
} }
) )
.padding(bottom = 10.dp.scaledHeight()).padding(top = 24.dp.scaledHeight()), .padding(bottom = 10.dp),
) { ) {
Column( Column(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
@@ -224,7 +233,9 @@ fun ConfigScreen(tunnelId: Int) {
ConfigurationToggle( ConfigurationToggle(
stringResource(id = R.string.show_amnezia_properties), stringResource(id = R.string.show_amnezia_properties),
checked = derivedConfigType.value == ConfigType.AMNEZIA, checked = derivedConfigType.value == ConfigType.AMNEZIA,
padding = screenPadding,
onCheckChanged = { configType = if (it) ConfigType.AMNEZIA else ConfigType.WIREGUARD }, onCheckChanged = { configType = if (it) ConfigType.AMNEZIA else ConfigType.WIREGUARD },
modifier = Modifier.focusRequester(focusRequester),
) )
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.tunnelName, value = uiState.tunnelName,
@@ -243,12 +254,12 @@ fun ConfigScreen(tunnelId: Int) {
.clickable { showAuthPrompt = true }, .clickable { showAuthPrompt = true },
value = uiState.interfaceProxy.privateKey, value = uiState.interfaceProxy.privateKey,
visualTransformation = visualTransformation =
if ((tunnelId == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated) { if ((tunnelId == Constants.MANUAL_TUNNEL_CONFIG_ID.toInt()) || isAuthenticated) {
VisualTransformation.None VisualTransformation.None
} else { } else {
PasswordVisualTransformation() PasswordVisualTransformation()
}, },
enabled = (tunnelId == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated, enabled = (tunnelId == Constants.MANUAL_TUNNEL_CONFIG_ID.toInt()) || isAuthenticated,
onValueChange = { value -> viewModel.onPrivateKeyChange(value) }, onValueChange = { value -> viewModel.onPrivateKeyChange(value) },
trailingIcon = { trailingIcon = {
IconButton( IconButton(
@@ -347,7 +358,8 @@ fun ConfigScreen(tunnelId: Int) {
hint = stringResource(R.string.junk_packet_count).lowercase(), hint = stringResource(R.string.junk_packet_count).lowercase(),
modifier = modifier =
Modifier Modifier
.fillMaxWidth(), .fillMaxWidth()
.focusRequester(focusRequester),
) )
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.interfaceProxy.junkPacketMinSize, value = uiState.interfaceProxy.junkPacketMinSize,
@@ -360,7 +372,8 @@ fun ConfigScreen(tunnelId: Int) {
).lowercase(), ).lowercase(),
modifier = modifier =
Modifier Modifier
.fillMaxWidth(), .fillMaxWidth()
.focusRequester(focusRequester),
) )
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.interfaceProxy.junkPacketMaxSize, value = uiState.interfaceProxy.junkPacketMaxSize,
@@ -373,7 +386,8 @@ fun ConfigScreen(tunnelId: Int) {
).lowercase(), ).lowercase(),
modifier = modifier =
Modifier Modifier
.fillMaxWidth(), .fillMaxWidth()
.focusRequester(focusRequester),
) )
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.interfaceProxy.initPacketJunkSize, value = uiState.interfaceProxy.initPacketJunkSize,
@@ -383,7 +397,8 @@ fun ConfigScreen(tunnelId: Int) {
hint = stringResource(R.string.init_packet_junk_size).lowercase(), hint = stringResource(R.string.init_packet_junk_size).lowercase(),
modifier = modifier =
Modifier Modifier
.fillMaxWidth(), .fillMaxWidth()
.focusRequester(focusRequester),
) )
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.interfaceProxy.responsePacketJunkSize, value = uiState.interfaceProxy.responsePacketJunkSize,
@@ -396,7 +411,8 @@ fun ConfigScreen(tunnelId: Int) {
).lowercase(), ).lowercase(),
modifier = modifier =
Modifier Modifier
.fillMaxWidth(), .fillMaxWidth()
.focusRequester(focusRequester),
) )
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.interfaceProxy.initPacketMagicHeader, value = uiState.interfaceProxy.initPacketMagicHeader,
@@ -409,7 +425,8 @@ fun ConfigScreen(tunnelId: Int) {
).lowercase(), ).lowercase(),
modifier = modifier =
Modifier Modifier
.fillMaxWidth(), .fillMaxWidth()
.focusRequester(focusRequester),
) )
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.interfaceProxy.responsePacketMagicHeader, value = uiState.interfaceProxy.responsePacketMagicHeader,
@@ -422,7 +439,8 @@ fun ConfigScreen(tunnelId: Int) {
).lowercase(), ).lowercase(),
modifier = modifier =
Modifier Modifier
.fillMaxWidth(), .fillMaxWidth()
.focusRequester(focusRequester),
) )
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.interfaceProxy.underloadPacketMagicHeader, value = uiState.interfaceProxy.underloadPacketMagicHeader,
@@ -435,7 +453,8 @@ fun ConfigScreen(tunnelId: Int) {
).lowercase(), ).lowercase(),
modifier = modifier =
Modifier Modifier
.fillMaxWidth(), .fillMaxWidth()
.focusRequester(focusRequester),
) )
ConfigurationTextBox( ConfigurationTextBox(
value = uiState.interfaceProxy.transportPacketMagicHeader, value = uiState.interfaceProxy.transportPacketMagicHeader,
@@ -448,7 +467,8 @@ fun ConfigScreen(tunnelId: Int) {
).lowercase(), ).lowercase(),
modifier = modifier =
Modifier Modifier
.fillMaxWidth(), .fillMaxWidth()
.focusRequester(focusRequester),
) )
} }
Row( Row(
@@ -519,15 +539,27 @@ fun ConfigScreen(tunnelId: Int) {
hint = stringResource(R.string.base64_key), hint = stringResource(R.string.base64_key),
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
ConfigurationTextBox( OutlinedTextField(
modifier =
Modifier
.fillMaxWidth()
.clickable { showAuthPrompt = true },
value = peer.preSharedKey, value = peer.preSharedKey,
visualTransformation =
if ((tunnelId == Constants.MANUAL_TUNNEL_CONFIG_ID.toInt()) || isAuthenticated) {
VisualTransformation.None
} else {
PasswordVisualTransformation()
},
enabled = (tunnelId == Constants.MANUAL_TUNNEL_CONFIG_ID.toInt()) || isAuthenticated || peer.preSharedKey.isEmpty(),
onValueChange = { value -> onValueChange = { value ->
viewModel.onPreSharedKeyChange(index, value) viewModel.onPreSharedKeyChange(index, value)
}, },
label = { Text(stringResource(R.string.preshared_key)) },
singleLine = true,
placeholder = { Text(stringResource(R.string.optional)) },
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
label = stringResource(R.string.preshared_key),
hint = stringResource(R.string.optional),
modifier = Modifier.fillMaxWidth(),
) )
OutlinedTextField( OutlinedTextField(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -18,6 +18,9 @@ data class ConfigUiState(
var tunnelName: String = "", var tunnelName: String = "",
val isAmneziaEnabled: Boolean = false, val isAmneziaEnabled: Boolean = false,
) { ) {
fun hasAmneziaProperties(): Boolean {
return this.interfaceProxy.junkPacketCount != ""
}
companion object { companion object {
fun from(config: Config): ConfigUiState { fun from(config: Config): ConfigUiState {
val proxyPeers = config.peers.map { PeerProxy.from(it) } val proxyPeers = config.peers.map { PeerProxy.from(it) }
@@ -74,7 +77,6 @@ data class ConfigUiState(
return from(config).copy( return from(config).copy(
tunnelName = tunnel.name, tunnelName = tunnel.name,
tunnel = tunnel, tunnel = tunnel,
isAmneziaEnabled = config.`interface`.junkPacketCount.isPresent,
) )
} }
} }
@@ -1,11 +1,8 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main package com.zaneschepke.wireguardautotunnel.ui.screens.main
import android.content.Intent import android.annotation.SuppressLint
import android.net.Uri
import android.net.VpnService import android.net.VpnService
import android.provider.Settings
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity.RESULT_OK import androidx.appcompat.app.AppCompatActivity.RESULT_OK
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
@@ -13,21 +10,19 @@ 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.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.overscroll import androidx.compose.foundation.overscroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material3.FabPosition import androidx.compose.material3.FabPosition
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -35,12 +30,11 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
@@ -50,7 +44,6 @@ import com.zaneschepke.wireguardautotunnel.ui.common.NestedScrollListener
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberFileImportLauncherForResult import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberFileImportLauncherForResult
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.AutoTunnelRowItem import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.AutoTunnelRowItem
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.GettingStartedLabel import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.GettingStartedLabel
@@ -59,17 +52,17 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.TunnelImpo
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.TunnelRowItem import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.TunnelRowItem
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.VpnDeniedDialog import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.VpnDeniedDialog
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.isBatteryOptimizationsDisabled
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground
import kotlinx.coroutines.delay
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState) { fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState, focusRequester: FocusRequester) {
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current val navController = LocalNavController.current
val clipboard = LocalClipboardManager.current
val snackbar = SnackbarController.current val snackbar = SnackbarController.current
var showBottomSheet by remember { mutableStateOf(false) } var showBottomSheet by remember { mutableStateOf(false) }
@@ -77,25 +70,30 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
var isFabVisible by rememberSaveable { mutableStateOf(true) } var isFabVisible by rememberSaveable { mutableStateOf(true) }
var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) } var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) }
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) } var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
val isRunningOnTv = remember { context.isRunningOnTv() }
val nestedScrollConnection = remember { val nestedScrollConnection = remember {
NestedScrollListener({ isFabVisible = false }, { isFabVisible = true }) NestedScrollListener({ isFabVisible = false }, { isFabVisible = true })
} }
val vpnActivity = val vpnActivityResultState =
rememberLauncherForActivityResult( rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult(), ActivityResultContracts.StartActivityForResult(),
onResult = { onResult = {
if (it.resultCode != RESULT_OK) showVpnPermissionDialog = true if (it.resultCode != RESULT_OK) showVpnPermissionDialog = true
}, },
) )
val batteryActivity =
rememberLauncherForActivityResult( LaunchedEffect(Unit) {
ActivityResultContracts.StartActivityForResult(), if (context.isRunningOnTv()) {
) { result: ActivityResult -> delay(Constants.FOCUS_REQUEST_DELAY)
viewModel.setBatteryOptimizeDisableShown() runCatching {
focusRequester.requestFocus()
}.onFailure {
delay(Constants.FOCUS_REQUEST_DELAY)
focusRequester.requestFocus()
}
} }
}
val tunnelFileImportResultLauncher = rememberFileImportLauncherForResult(onNoFileExplorer = { val tunnelFileImportResultLauncher = rememberFileImportLauncherForResult(onNoFileExplorer = {
snackbar.showMessage( snackbar.showMessage(
@@ -118,7 +116,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
InfoDialog( InfoDialog(
onDismiss = { showDeleteTunnelAlertDialog = false }, onDismiss = { showDeleteTunnelAlertDialog = false },
onAttest = { onAttest = {
selectedTunnel?.let { viewModel.onDelete(it) } selectedTunnel?.let { viewModel.onDelete(it, context) }
showDeleteTunnelAlertDialog = false showDeleteTunnelAlertDialog = false
selectedTunnel = null selectedTunnel = null
}, },
@@ -128,35 +126,15 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
) )
} }
fun requestBatteryOptimizationsDisabled() {
val intent =
Intent().apply {
action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
data = Uri.parse("package:${context.packageName}")
}
batteryActivity.launch(intent)
}
fun onAutoTunnelToggle() {
if (!uiState.generalState.isBatteryOptimizationDisableShown &&
!context.isBatteryOptimizationsDisabled() && !isRunningOnTv
) {
return requestBatteryOptimizationsDisabled()
}
val intent = if (!uiState.settings.isKernelEnabled) {
VpnService.prepare(context)
} else {
null
}
if (intent != null) return vpnActivity.launch(intent)
viewModel.onToggleAutoTunnel()
}
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) { fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
val intent = if (uiState.settings.isKernelEnabled) null else VpnService.prepare(context) val intent = if (uiState.settings.isKernelEnabled) null else VpnService.prepare(context)
if (intent != null) return vpnActivity.launch(intent) if (intent != null) return vpnActivityResultState.launch(intent)
if (!checked) viewModel.onTunnelStop(tunnel).also { return } if (!checked) viewModel.onTunnelStop(tunnel).also { return }
viewModel.onTunnelStart(tunnel, uiState.settings.isKernelEnabled) if (uiState.settings.isKernelEnabled) {
context.startTunnelBackground(tunnel.id)
} else {
viewModel.onTunnelStart(tunnel)
}
} }
Scaffold( Scaffold(
@@ -171,49 +149,23 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
}, },
floatingActionButtonPosition = FabPosition.End, floatingActionButtonPosition = FabPosition.End,
floatingActionButton = { floatingActionButton = {
if (!isRunningOnTv) { ScrollDismissFab({
ScrollDismissFab({ val icon = Icons.Filled.Add
val icon = Icons.Filled.Add Icon(
Icon( imageVector = icon,
imageVector = icon, contentDescription = icon.name,
contentDescription = icon.name, tint = MaterialTheme.colorScheme.onPrimary,
tint = MaterialTheme.colorScheme.onPrimary,
)
}, isVisible = isFabVisible, onClick = {
showBottomSheet = true
})
}
},
topBar = {
if (isRunningOnTv) {
TopNavBar(
showBack = false,
title = stringResource(R.string.app_name),
trailing = {
IconButton(onClick = {
showBottomSheet = true
}) {
val icon = Icons.Outlined.Add
Icon(
imageVector = icon,
contentDescription = icon.name,
)
}
},
) )
} }, focusRequester, isVisible = isFabVisible, onClick = {
showBottomSheet = true
})
}, },
) { padding -> ) {
TunnelImportSheet( TunnelImportSheet(
showBottomSheet, showBottomSheet,
onDismiss = { showBottomSheet = false }, onDismiss = { showBottomSheet = false },
onFileClick = { tunnelFileImportResultLauncher.launch(Constants.ALLOWED_TV_FILE_TYPES) }, onFileClick = { tunnelFileImportResultLauncher.launch(Constants.ALLOWED_TV_FILE_TYPES) },
onQrClick = { requestPermissionLauncher.launch(android.Manifest.permission.CAMERA) }, onQrClick = { requestPermissionLauncher.launch(android.Manifest.permission.CAMERA) },
onClipboardClick = {
clipboard.getText()?.text?.let {
viewModel.onClipboardImport(it)
}
},
onManualImportClick = { onManualImportClick = {
navController.navigate( navController.navigate(
Route.Config(Constants.MANUAL_TUNNEL_CONFIG_ID), Route.Config(Constants.MANUAL_TUNNEL_CONFIG_ID),
@@ -222,10 +174,10 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
) )
LazyColumn( LazyColumn(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(5.dp.scaledHeight(), Alignment.Top), verticalArrangement = Arrangement.Top,
modifier = modifier =
Modifier Modifier
.fillMaxSize().padding(padding) .fillMaxSize()
.overscroll(ScrollableDefaults.overscrollEffect()) .overscroll(ScrollableDefaults.overscrollEffect())
.nestedScroll(nestedScrollConnection), .nestedScroll(nestedScrollConnection),
state = rememberLazyListState(0, uiState.tunnels.count()), state = rememberLazyListState(0, uiState.tunnels.count()),
@@ -237,11 +189,10 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
item { item {
GettingStartedLabel(onClick = { context.openWebUrl(it) }) GettingStartedLabel(onClick = { context.openWebUrl(it) })
} }
} else { }
if (uiState.settings.isAutoTunnelEnabled) {
item { item {
AutoTunnelRowItem(uiState, { AutoTunnelRowItem(uiState.settings, { viewModel.onToggleAutoTunnelingPause() }, focusRequester)
onAutoTunnelToggle()
})
} }
} }
items( items(
@@ -264,6 +215,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
onDelete = { showDeleteTunnelAlertDialog = true }, onDelete = { showDeleteTunnelAlertDialog = true },
onCopy = { viewModel.onCopyTunnel(tunnel) }, onCopy = { viewModel.onCopyTunnel(tunnel) },
onSwitchClick = { onTunnelToggle(it, tunnel) }, onSwitchClick = { onTunnelToggle(it, tunnel) },
focusRequester = focusRequester,
) )
} }
} }
@@ -30,24 +30,26 @@ import timber.log.Timber
import java.io.InputStream import java.io.InputStream
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider
@HiltViewModel @HiltViewModel
class MainViewModel class MainViewModel
@Inject @Inject
constructor( constructor(
private val appDataRepository: AppDataRepository, private val appDataRepository: AppDataRepository,
private val tunnelService: Provider<TunnelService>, val tunnelService: TunnelService,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher, @IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val serviceManager: ServiceManager,
) : ViewModel() { ) : ViewModel() {
fun onDelete(tunnel: TunnelConfig) { private fun stopWatcherService(context: Context) {
ServiceManager.stopWatcherService(context)
}
fun onDelete(tunnel: TunnelConfig, context: Context) {
viewModelScope.launch { viewModelScope.launch {
val settings = appDataRepository.settings.getSettings() val settings = appDataRepository.settings.getSettings()
val isPrimary = tunnel.isPrimaryTunnel val isPrimary = tunnel.isPrimaryTunnel
if (appDataRepository.tunnels.count() == 1 || isPrimary) { if (appDataRepository.tunnels.count() == 1 || isPrimary) {
serviceManager.stopAutoTunnel() stopWatcherService(context)
resetTunnelSetting(settings) resetTunnelSetting(settings)
} }
appDataRepository.tunnels.delete(tunnel) appDataRepository.tunnels.delete(tunnel)
@@ -67,14 +69,14 @@ constructor(
appDataRepository.appState.setTunnelStatsExpanded(expanded) appDataRepository.appState.setTunnelStatsExpanded(expanded)
} }
fun onTunnelStart(tunnelConfig: TunnelConfig, background: Boolean) = viewModelScope.launch { fun onTunnelStart(tunnelConfig: TunnelConfig) = viewModelScope.launch {
Timber.i("Starting tunnel ${tunnelConfig.name}") Timber.i("Starting tunnel ${tunnelConfig.name}")
tunnelService.get().startTunnel(tunnelConfig, background) tunnelService.startTunnel(tunnelConfig)
} }
fun onTunnelStop(tunnel: TunnelConfig) = viewModelScope.launch { fun onTunnelStop(tunnel: TunnelConfig) = viewModelScope.launch {
Timber.i("Stopping active tunnel") Timber.i("Stopping active tunnel")
tunnelService.get().stopTunnel(tunnel) tunnelService.stopTunnel(tunnel)
} }
private fun generateQrCodeDefaultName(config: String): String { private fun generateQrCodeDefaultName(config: String): String {
@@ -158,21 +160,6 @@ constructor(
} }
} }
fun onToggleAutoTunnel() = viewModelScope.launch {
val settings = appDataRepository.settings.getSettings()
val toggled = !settings.isAutoTunnelEnabled
if (toggled) {
serviceManager.startAutoTunnel(false)
} else {
serviceManager.stopAutoTunnel()
}
appDataRepository.settings.save(
settings.copy(
isAutoTunnelEnabled = toggled,
),
)
}
private suspend fun saveTunnelsFromZipUri(uri: Uri, context: Context) { private suspend fun saveTunnelsFromZipUri(uri: Uri, context: Context) {
ZipInputStream(getInputStreamFromUri(uri, context)).use { zip -> ZipInputStream(getInputStreamFromUri(uri, context)).use { zip ->
generateSequence { zip.nextEntry } generateSequence { zip.nextEntry }
@@ -194,15 +181,18 @@ constructor(
} }
} }
fun setBatteryOptimizeDisableShown() = viewModelScope.launch {
appDataRepository.appState.setBatteryOptimizationDisableShown(true)
}
private suspend fun saveTunnelFromConfUri(name: String, uri: Uri, context: Context) { private suspend fun saveTunnelFromConfUri(name: String, uri: Uri, context: Context) {
val stream = getInputStreamFromUri(uri, context) ?: throw FileReadException val stream = getInputStreamFromUri(uri, context) ?: throw FileReadException
saveTunnelConfigFromStream(stream, name) saveTunnelConfigFromStream(stream, name)
} }
fun onToggleAutoTunnelingPause() = viewModelScope.launch {
val settings = appDataRepository.settings.getSettings()
appDataRepository.settings.save(
settings.copy(isAutoTunnelPaused = !settings.isAutoTunnelPaused),
)
}
private fun saveTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch { private fun saveTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch {
appDataRepository.tunnels.save(tunnelConfig) appDataRepository.tunnels.save(tunnelConfig)
} }
@@ -261,14 +251,4 @@ constructor(
), ),
) )
} }
fun onClipboardImport(config: String) = viewModelScope.launch(ioDispatcher) {
runCatching {
val amConfig = TunnelConfig.configFromAmQuick(config)
val tunnelConfig = TunnelConfig.tunnelConfigFromAmConfig(amConfig, makeTunnelNameUnique(generateQrCodeDefaultName(config)))
saveTunnel(tunnelConfig)
}.onFailure {
SnackbarController.showMessage(StringValue.StringResource(R.string.error_file_format))
}
}
} }
@@ -4,23 +4,43 @@ import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Bolt import androidx.compose.material.icons.rounded.Bolt
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale import androidx.compose.ui.draw.scale
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.text.buildAnnotatedString
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.AppUiState import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.ui.common.ExpandingRowListItem import com.zaneschepke.wireguardautotunnel.ui.common.ExpandingRowListItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
@Composable @Composable
fun AutoTunnelRowItem(appUiState: AppUiState, onToggle: () -> Unit) { fun AutoTunnelRowItem(settings: Settings, onToggle: () -> Unit, focusRequester: FocusRequester) {
val context = LocalContext.current val context = LocalContext.current
val itemFocusRequester = remember { FocusRequester() }
val autoTunnelingLabel =
buildAnnotatedString {
append(stringResource(id = R.string.auto_tunneling))
append(": ")
if (settings.isAutoTunnelPaused) {
append(
stringResource(id = R.string.paused),
)
} else {
append(
stringResource(id = R.string.active),
)
}
}
ExpandingRowListItem( ExpandingRowListItem(
leading = { leading = {
val icon = Icons.Rounded.Bolt val icon = Icons.Rounded.Bolt
@@ -29,29 +49,30 @@ fun AutoTunnelRowItem(appUiState: AppUiState, onToggle: () -> Unit) {
icon.name, icon.name,
modifier = modifier =
Modifier Modifier
.size(16.dp).scale(1.5f), .size(iconSize).scale(1.5f),
tint = tint =
if (!appUiState.autoTunnelActive) { if (settings.isAutoTunnelPaused) {
Color.Gray Color.Gray
} else { } else {
SilverTree SilverTree
}, },
) )
}, },
text = stringResource(R.string.auto_tunneling), text = autoTunnelingLabel.text,
trailing = { trailing = {
ScaledSwitch( TextButton(
appUiState.settings.isAutoTunnelEnabled, modifier = Modifier.focusRequester(itemFocusRequester),
onClick = { onClick = { onToggle() },
onToggle() ) {
}, Text(stringResource(id = if (settings.isAutoTunnelPaused) R.string.resume else R.string.pause))
) }
}, },
onClick = { onClick = {
if (context.isRunningOnTv()) { if (context.isRunningOnTv()) {
onToggle() itemFocusRequester.requestFocus()
} }
}, },
isExpanded = false, isExpanded = false,
focusRequester = focusRequester,
) )
} }
@@ -9,16 +9,19 @@ import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
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.unit.dp import androidx.compose.ui.unit.dp
@Composable @Composable
fun ScrollDismissFab(icon: @Composable () -> Unit, isVisible: Boolean, onClick: () -> Unit) { fun ScrollDismissFab(icon: @Composable () -> Unit, focusRequester: FocusRequester, isVisible: Boolean, onClick: () -> Unit) {
AnimatedVisibility( AnimatedVisibility(
visible = isVisible, visible = isVisible,
enter = slideInVertically(initialOffsetY = { it * 2 }), enter = slideInVertically(initialOffsetY = { it * 2 }),
exit = slideOutVertically(targetOffsetY = { it * 2 }), exit = slideOutVertically(targetOffsetY = { it * 2 }),
modifier = modifier =
Modifier Modifier
.focusRequester(focusRequester)
.focusGroup(), .focusGroup(),
) { ) {
FloatingActionButton( FloatingActionButton(
@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ContentPasteGo
import androidx.compose.material.icons.filled.Create import androidx.compose.material.icons.filled.Create
import androidx.compose.material.icons.filled.FileOpen import androidx.compose.material.icons.filled.FileOpen
import androidx.compose.material.icons.filled.QrCode import androidx.compose.material.icons.filled.QrCode
@@ -23,17 +22,9 @@ import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
// TODO refactor this component
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun TunnelImportSheet( fun TunnelImportSheet(show: Boolean, onDismiss: () -> Unit, onFileClick: () -> Unit, onQrClick: () -> Unit, onManualImportClick: () -> Unit) {
show: Boolean,
onDismiss: () -> Unit,
onFileClick: () -> Unit,
onQrClick: () -> Unit,
onManualImportClick: () -> Unit,
onClipboardClick: () -> Unit,
) {
val sheetState = rememberModalBottomSheetState() val sheetState = rememberModalBottomSheetState()
val context = LocalContext.current val context = LocalContext.current
@@ -86,28 +77,6 @@ fun TunnelImportSheet(
modifier = Modifier.padding(10.dp), modifier = Modifier.padding(10.dp),
) )
} }
HorizontalDivider()
Row(
modifier =
Modifier
.fillMaxWidth()
.clickable {
onDismiss()
onClipboardClick()
}
.padding(10.dp),
) {
val icon = Icons.Filled.ContentPasteGo
Icon(
icon,
contentDescription = icon.name,
modifier = Modifier.padding(10.dp),
)
Text(
stringResource(id = R.string.add_from_clipboard),
modifier = Modifier.padding(10.dp),
)
}
} }
HorizontalDivider() HorizontalDivider()
Row( Row(
@@ -13,6 +13,7 @@ import androidx.compose.material.icons.rounded.Smartphone
import androidx.compose.material.icons.rounded.Star import androidx.compose.material.icons.rounded.Star
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.Switch
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -22,15 +23,14 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
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.unit.dp
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
import com.zaneschepke.wireguardautotunnel.ui.Route import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.ExpandingRowListItem import com.zaneschepke.wireguardautotunnel.ui.common.ExpandingRowListItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
import com.zaneschepke.wireguardautotunnel.util.extensions.asColor import com.zaneschepke.wireguardautotunnel.util.extensions.asColor
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
@@ -46,6 +46,7 @@ fun TunnelRowItem(
onCopy: () -> Unit, onCopy: () -> Unit,
onDelete: () -> Unit, onDelete: () -> Unit,
onSwitchClick: (checked: Boolean) -> Unit, onSwitchClick: (checked: Boolean) -> Unit,
focusRequester: FocusRequester,
) { ) {
val leadingIconColor = if (!isActive) Color.Gray else vpnState.statistics.asColor() val leadingIconColor = if (!isActive) Color.Gray else vpnState.statistics.asColor()
val context = LocalContext.current val context = LocalContext.current
@@ -55,19 +56,20 @@ fun TunnelRowItem(
val itemFocusRequester = remember { FocusRequester() } val itemFocusRequester = remember { FocusRequester() }
ExpandingRowListItem( ExpandingRowListItem(
leading = { leading = {
val circleIcon = Icons.Rounded.Circle
val icon = val icon =
if (tunnel.isPrimaryTunnel) { if (tunnel.isPrimaryTunnel) {
Icons.Rounded.Star Icons.Rounded.Star
} else if (tunnel.isMobileDataTunnel) { } else if (tunnel.isMobileDataTunnel) {
Icons.Rounded.Smartphone Icons.Rounded.Smartphone
} else { } else {
Icons.Rounded.Circle circleIcon
} }
Icon( Icon(
icon, icon,
icon.name, icon.name,
tint = leadingIconColor, tint = leadingIconColor,
modifier = Modifier.size(16.dp), modifier = Modifier.size(iconSize),
) )
}, },
text = tunnel.name, text = tunnel.name,
@@ -87,6 +89,7 @@ fun TunnelRowItem(
}, },
isExpanded = expanded && isActive, isExpanded = expanded && isActive,
expanded = { if (isActive && expanded) TunnelStatisticsRow(vpnState.statistics, tunnel) }, expanded = { if (isActive && expanded) TunnelStatisticsRow(vpnState.statistics, tunnel) },
focusRequester = focusRequester,
trailing = { trailing = {
if ( if (
isSelected && isSelected &&
@@ -140,6 +143,7 @@ fun TunnelRowItem(
) )
} }
IconButton( IconButton(
modifier = Modifier.focusRequester(focusRequester),
onClick = { onClick = {
if (isActive) { if (isActive) {
onClick() onClick()
@@ -177,17 +181,21 @@ fun TunnelRowItem(
icon.name, icon.name,
) )
} }
ScaledSwitch( Switch(
modifier = Modifier.focusRequester(itemFocusRequester), modifier = Modifier.focusRequester(itemFocusRequester),
checked = isActive, checked = isActive,
onClick = onSwitchClick, onCheckedChange = { checked ->
onSwitchClick(checked)
},
) )
} }
} else { } else {
ScaledSwitch( Switch(
modifier = Modifier.focusRequester(itemFocusRequester), modifier = Modifier.focusRequester(itemFocusRequester),
checked = isActive, checked = isActive,
onClick = onSwitchClick, onCheckedChange = { checked ->
onSwitchClick(checked)
},
) )
} }
} }
@@ -46,18 +46,14 @@ fun TunnelStatisticsRow(statistics: TunnelStatistics?, tunnelConfig: TunnelConfi
Column( Column(
verticalArrangement = Arrangement.spacedBy(10.dp), verticalArrangement = Arrangement.spacedBy(10.dp),
) { ) {
Text( Text(stringResource(R.string.peer).lowercase() + ": $peerId", style = MaterialTheme.typography.bodySmall)
stringResource(R.string.peer).lowercase() + ": $peerId", Text("tx: $peerTxMB MB", style = MaterialTheme.typography.bodySmall)
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline,
)
Text("tx: $peerTxMB MB", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.outline)
} }
Column( Column(
verticalArrangement = Arrangement.spacedBy(10.dp), verticalArrangement = Arrangement.spacedBy(10.dp),
) { ) {
Text(stringResource(R.string.handshake) + ": $handshake", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.outline) Text(stringResource(R.string.handshake) + ": $handshake", style = MaterialTheme.typography.bodySmall)
Text("rx: $peerRxMB MB", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.outline) Text("rx: $peerRxMB MB", style = MaterialTheme.typography.bodySmall)
} }
} }
} }
@@ -1,25 +1,33 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.options package com.zaneschepke.wireguardautotunnel.ui.screens.options
import android.annotation.SuppressLint
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
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.Row import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
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.filled.Close
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material.icons.outlined.Edit import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material.icons.outlined.NetworkPing
import androidx.compose.material.icons.outlined.PhoneAndroid
import androidx.compose.material.icons.outlined.Security
import androidx.compose.material.icons.outlined.Star
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
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -29,239 +37,298 @@ 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.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.AppUiState import com.zaneschepke.wireguardautotunnel.ui.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.Route import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.config.SubmitConfigurationTextBox import com.zaneschepke.wireguardautotunnel.ui.common.config.SubmitConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.TrustedNetworkTextBox import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.ScrollDismissFab
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.WildcardsLabel import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.WildcardSupportingLabel
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth import kotlinx.coroutines.delay
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), appUiState: AppUiState, tunnelId: Int) { fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), focusRequester: FocusRequester, appUiState: AppUiState, tunnelId: Int) {
val scrollState = rememberScrollState()
val context = LocalContext.current
val navController = LocalNavController.current val navController = LocalNavController.current
val config = appUiState.tunnels.first { it.id == tunnelId } val config = appUiState.tunnels.first { it.id == tunnelId }
val interactionSource = remember { MutableInteractionSource() }
val focusManager = LocalFocusManager.current
val screenPadding = 5.dp
val fillMaxWidth = .85f
var currentText by remember { mutableStateOf("") } var currentText by remember { mutableStateOf("") }
LaunchedEffect(config.tunnelNetworks) { LaunchedEffect(Unit) {
currentText = "" if (context.isRunningOnTv()) {
delay(Constants.FOCUS_REQUEST_DELAY)
kotlin.runCatching {
focusRequester.requestFocus()
}.onFailure {
delay(Constants.FOCUS_REQUEST_DELAY)
focusRequester.requestFocus()
}
}
} }
fun saveTrustedSSID() {
if (currentText.isNotEmpty()) {
optionsViewModel.onSaveRunSSID(currentText, config)
currentText = ""
}
}
Scaffold( Scaffold(
topBar = { floatingActionButton = {
TopNavBar(config.name, trailing = { ScrollDismissFab(icon = {
IconButton(onClick = { val icon = Icons.Filled.Edit
navController.navigate( Icon(
Route.Config(config.id), imageVector = icon,
) contentDescription = icon.name,
}) { tint = MaterialTheme.colorScheme.onPrimary,
val icon = Icons.Outlined.Edit )
Icon( }, focusRequester, isVisible = true, onClick = {
imageVector = icon, navController.navigate(
contentDescription = icon.name, Route.Config(config.id),
) )
}
}) })
}, },
) { ) {
Column( Column(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top), verticalArrangement = Arrangement.Top,
modifier = modifier =
Modifier Modifier
.fillMaxSize() .fillMaxSize()
.padding(it) .verticalScroll(scrollState)
.verticalScroll(rememberScrollState()) .clickable(
.padding(top = 24.dp.scaledHeight()) indication = null,
.padding(horizontal = 24.dp.scaledWidth()), interactionSource = interactionSource,
) {
focusManager.clearFocus()
},
) { ) {
GroupLabel(stringResource(R.string.auto_tunneling)) Surface(
SurfaceSelectionGroupButton( tonalElevation = 2.dp,
buildList { shadowElevation = 2.dp,
addAll( shape = RoundedCornerShape(12.dp),
listOf( color = MaterialTheme.colorScheme.surface,
SelectionItem( modifier =
Icons.Outlined.Star, (
title = { if (context.isRunningOnTv()) {
Text( Modifier
stringResource(R.string.primary_tunnel), .height(IntrinsicSize.Min)
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), .fillMaxWidth(fillMaxWidth)
) .padding(top = 10.dp)
}, } else {
description = { Modifier
Text( .fillMaxWidth(fillMaxWidth)
stringResource(R.string.set_primary_tunnel), .padding(top = 20.dp)
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.outline),
)
},
trailing = {
ScaledSwitch(
config.isPrimaryTunnel,
onClick = { optionsViewModel.onTogglePrimaryTunnel(config) },
)
},
onClick = { optionsViewModel.onTogglePrimaryTunnel(config) },
),
SelectionItem(
Icons.Outlined.PhoneAndroid,
title = {
Text(
stringResource(R.string.mobile_tunnel),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
description = {
Text(
stringResource(R.string.mobile_data_tunnel),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.outline),
)
},
trailing = {
ScaledSwitch(
config.isMobileDataTunnel,
onClick = { optionsViewModel.onToggleIsMobileDataTunnel(config) },
)
},
onClick = { optionsViewModel.onToggleIsMobileDataTunnel(config) },
),
SelectionItem(
Icons.Outlined.NetworkPing,
title = {
Text(
stringResource(R.string.restart_on_ping),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ScaledSwitch(
checked = config.isPingEnabled,
onClick = { optionsViewModel.onToggleRestartOnPing(config) },
)
},
onClick = { optionsViewModel.onToggleRestartOnPing(config) },
),
),
)
if (config.isPingEnabled || appUiState.settings.isPingEnabled) {
add(
SelectionItem(
title = {},
description = {
SubmitConfigurationTextBox(
config.pingIp,
stringResource(R.string.set_custom_ping_ip),
stringResource(R.string.default_ping_ip),
isErrorValue = { !it.isNullOrBlank() && !it.isValidIpv4orIpv6Address() },
onSubmit = {
optionsViewModel.saveTunnelChanges(
config.copy(pingIp = it.ifBlank { null }),
)
},
)
fun isSecondsError(seconds: String?): Boolean {
return seconds?.let { value -> if (value.isBlank()) false else value.toLong() >= Long.MAX_VALUE / 1000 } ?: false
}
SubmitConfigurationTextBox(
config.pingInterval?.let { (it / 1000).toString() },
stringResource(R.string.set_custom_ping_internal),
"(${stringResource(R.string.optional_default)} ${Constants.PING_INTERVAL / 1000})",
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Done,
),
isErrorValue = ::isSecondsError,
onSubmit = {
optionsViewModel.saveTunnelChanges(
config.copy(pingInterval = if (it.isBlank()) null else it.toLong() * 1000),
)
},
)
SubmitConfigurationTextBox(
config.pingCooldown?.let { (it / 1000).toString() },
stringResource(R.string.set_custom_ping_cooldown),
"(${stringResource(R.string.optional_default)} ${Constants.PING_COOLDOWN / 1000})",
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
),
isErrorValue = ::isSecondsError,
onSubmit = {
optionsViewModel.saveTunnelChanges(
config.copy(pingCooldown = if (it.isBlank()) null else it.toLong() * 1000),
)
},
)
},
),
)
} }
add( )
SelectionItem( .padding(bottom = 10.dp),
title = { ) {
Row( Column(
verticalAlignment = Alignment.CenterVertically, horizontalAlignment = Alignment.Start,
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp.scaledHeight()), verticalArrangement = Arrangement.Top,
) { modifier = Modifier.padding(15.dp),
Row( ) {
verticalAlignment = Alignment.CenterVertically, SectionTitle(
modifier = Modifier title = stringResource(id = R.string.general),
.weight(4f, false) padding = screenPadding,
.fillMaxWidth(), )
) { ConfigurationToggle(
val icon = Icons.Outlined.Security stringResource(R.string.set_primary_tunnel),
Icon( enabled = true,
icon, checked = config.isPrimaryTunnel,
icon.name, modifier =
modifier = Modifier.size(iconSize), Modifier
) .focusRequester(focusRequester),
Column( padding = screenPadding,
horizontalAlignment = Alignment.Start, onCheckChanged = { optionsViewModel.onTogglePrimaryTunnel(config) },
verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically), )
modifier = Modifier }
.fillMaxWidth() }
.padding(start = 16.dp.scaledWidth()) Surface(
.padding(vertical = 6.dp.scaledHeight()), tonalElevation = 2.dp,
) { shadowElevation = 2.dp,
Text( shape = RoundedCornerShape(12.dp),
stringResource(R.string.use_tunnel_on_wifi_name), color = MaterialTheme.colorScheme.surface,
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), modifier =
) (
if (context.isRunningOnTv()) {
Modifier
.height(IntrinsicSize.Min)
.fillMaxWidth(fillMaxWidth)
.padding(top = 10.dp)
} else {
Modifier
.fillMaxWidth(fillMaxWidth)
.padding(top = 20.dp)
}
)
.padding(bottom = 10.dp),
) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier = Modifier.padding(15.dp),
) {
SectionTitle(
title = stringResource(id = R.string.auto_tunneling),
padding = screenPadding,
)
ConfigurationToggle(
stringResource(R.string.mobile_data_tunnel),
enabled = true,
checked = config.isMobileDataTunnel,
padding = screenPadding,
onCheckChanged = { optionsViewModel.onToggleIsMobileDataTunnel(config) },
)
Column {
FlowRow(
modifier =
Modifier
.padding(screenPadding)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(5.dp),
) {
config.tunnelNetworks.forEach { ssid ->
ClickableIconButton(
onClick = {
if (context.isRunningOnTv()) {
focusRequester.requestFocus()
optionsViewModel.onDeleteRunSSID(ssid, config)
} }
},
onIconClick = {
if (context.isRunningOnTv()) focusRequester.requestFocus()
optionsViewModel.onDeleteRunSSID(ssid, config)
},
text = ssid,
icon = Icons.Filled.Close,
enabled = true,
)
}
if (config.tunnelNetworks.isEmpty()) {
Text(
stringResource(R.string.no_wifi_names_configured),
fontStyle = FontStyle.Italic,
color = MaterialTheme.colorScheme.onSurface,
)
}
}
OutlinedTextField(
enabled = true,
value = currentText,
onValueChange = { currentText = it },
label = { Text(stringResource(id = R.string.use_tunnel_on_wifi_name)) },
supportingText = { WildcardSupportingLabel { context.openWebUrl(it) } },
modifier =
Modifier
.padding(
start = screenPadding,
top = 5.dp,
bottom = 10.dp,
),
maxLines = 1,
keyboardOptions =
KeyboardOptions(
capitalization = KeyboardCapitalization.None,
imeAction = ImeAction.Done,
),
keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }),
trailingIcon = {
if (currentText != "") {
IconButton(onClick = { saveTrustedSSID() }) {
Icon(
imageVector = Icons.Outlined.Add,
contentDescription = stringResource(R.string.save_changes),
tint = MaterialTheme.colorScheme.primary,
)
} }
} }
}, },
description = { )
TrustedNetworkTextBox( ConfigurationToggle(
config.tunnelNetworks, stringResource(R.string.restart_on_ping),
onDelete = { optionsViewModel.onDeleteRunSSID(it, config) }, enabled = !appUiState.settings.isPingEnabled,
currentText = currentText, checked = config.isPingEnabled || appUiState.settings.isPingEnabled,
onSave = { optionsViewModel.onSaveRunSSID(it, config) }, padding = screenPadding,
onValueChange = { currentText = it }, onCheckChanged = { optionsViewModel.onToggleRestartOnPing(config) },
supporting = { )
if (appUiState.settings.isWildcardsEnabled) { if (config.isPingEnabled || appUiState.settings.isPingEnabled) {
WildcardsLabel() SubmitConfigurationTextBox(
} config.pingIp,
}, stringResource(R.string.set_custom_ping_ip),
) stringResource(R.string.default_ping_ip),
}, focusRequester,
), isErrorValue = { !it.isNullOrBlank() && !it.isValidIpv4orIpv6Address() },
) onSubmit = {
}, optionsViewModel.saveTunnelChanges(
) config.copy(pingIp = it.ifBlank { null }),
)
},
)
fun isSecondsError(seconds: String?): Boolean {
return seconds?.let { value -> if (value.isBlank()) false else value.toLong() >= Long.MAX_VALUE / 1000 } ?: false
}
SubmitConfigurationTextBox(
config.pingInterval?.let { (it / 1000).toString() },
stringResource(R.string.set_custom_ping_internal),
"(${stringResource(R.string.optional_default)} ${Constants.PING_INTERVAL / 1000})",
focusRequester,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Done,
),
isErrorValue = ::isSecondsError,
onSubmit = {
optionsViewModel.saveTunnelChanges(
config.copy(pingInterval = if (it.isBlank()) null else it.toLong() * 1000),
)
},
)
SubmitConfigurationTextBox(
config.pingCooldown?.let { (it / 1000).toString() },
stringResource(R.string.set_custom_ping_cooldown),
"(${stringResource(R.string.optional_default)} ${Constants.PING_COOLDOWN / 1000})",
focusRequester,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
),
isErrorValue = ::isSecondsError,
onSubmit = {
optionsViewModel.saveTunnelChanges(
config.copy(pingCooldown = if (it.isBlank()) null else it.toLong() * 1000),
)
},
)
}
}
}
}
} }
} }
} }
@@ -32,7 +32,6 @@ constructor(
} }
fun onSaveRunSSID(ssid: String, tunnelConfig: TunnelConfig) = viewModelScope.launch { fun onSaveRunSSID(ssid: String, tunnelConfig: TunnelConfig) = viewModelScope.launch {
if (ssid.isBlank()) return@launch
val trimmed = ssid.trim() val trimmed = ssid.trim()
val tunnelsWithName = appDataRepository.tunnels.findByTunnelNetworksName(trimmed) val tunnelsWithName = appDataRepository.tunnels.findByTunnelNetworksName(trimmed)
@@ -9,9 +9,11 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.journeyapps.barcodescanner.CompoundBarcodeView import com.journeyapps.barcodescanner.CompoundBarcodeView
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
@OptIn(ExperimentalPermissionsApi::class)
@Composable @Composable
fun ScannerScreen(viewModel: ScannerViewModel = hiltViewModel()) { fun ScannerScreen(viewModel: ScannerViewModel = hiltViewModel()) {
val context = LocalContext.current val context = LocalContext.current
@@ -9,6 +9,7 @@ import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.util.NumberUtils import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import com.zaneschepke.wireguardautotunnel.util.StringValue import com.zaneschepke.wireguardautotunnel.util.StringValue
import com.zaneschepke.wireguardautotunnel.util.extensions.toWgQuickString
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
@@ -44,7 +45,10 @@ constructor(
fun onTunnelQrResult(result: String) = viewModelScope.launch(ioDispatcher) { fun onTunnelQrResult(result: String) = viewModelScope.launch(ioDispatcher) {
kotlin.runCatching { kotlin.runCatching {
val amConfig = TunnelConfig.configFromAmQuick(result) val amConfig = TunnelConfig.configFromAmQuick(result)
val tunnelConfig = TunnelConfig.tunnelConfigFromAmConfig(amConfig, makeTunnelNameUnique(generateQrCodeDefaultName(result))) val amQuick = amConfig.toAwgQuickString(true)
val wgQuick = amConfig.toWgQuickString()
val tunnelName = makeTunnelNameUnique(generateQrCodeDefaultName(result))
val tunnelConfig = TunnelConfig(name = tunnelName, wgQuick = wgQuick, amQuick = amQuick)
appDataRepository.tunnels.save(tunnelConfig) appDataRepository.tunnels.save(tunnelConfig)
_success.emit(true) _success.emit(true)
}.onFailure { }.onFailure {
@@ -1,76 +1,246 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings package com.zaneschepke.wireguardautotunnel.ui.screens.settings
import android.Manifest
import android.content.Context.POWER_SERVICE
import android.content.Intent
import android.net.Uri
import android.net.VpnService
import android.os.Build
import android.os.PowerManager
import android.provider.Settings
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity.RESULT_OK
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
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.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
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.automirrored.outlined.ViewQuilt import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.AppShortcut import androidx.compose.material.icons.outlined.Add
import androidx.compose.material.icons.outlined.AdminPanelSettings import androidx.compose.material3.Icon
import androidx.compose.material.icons.outlined.Bolt import androidx.compose.material3.IconButton
import androidx.compose.material.icons.outlined.Code
import androidx.compose.material.icons.outlined.FolderZip
import androidx.compose.material.icons.outlined.Notifications
import androidx.compose.material.icons.outlined.Pin
import androidx.compose.material.icons.outlined.Restore
import androidx.compose.material.icons.outlined.VpnLock
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf 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.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.AppUiState import com.zaneschepke.wireguardautotunnel.ui.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.Route import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.ForwardButton import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
import com.zaneschepke.wireguardautotunnel.ui.theme.topPadding import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.VpnDeniedDialog
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackgroundLocationDialog
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackgroundLocationDisclosure
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LocationServicesDialog
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.WildcardSupportingLabel
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.launchNotificationSettings import com.zaneschepke.wireguardautotunnel.util.extensions.launchAppSettings
import com.zaneschepke.wireguardautotunnel.util.extensions.launchVpnSettings import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
import xyz.teamgravity.pin_lock_compose.PinManager import xyz.teamgravity.pin_lock_compose.PinManager
@OptIn(
ExperimentalPermissionsApi::class,
ExperimentalLayoutApi::class,
)
@Composable @Composable
fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel: AppViewModel, uiState: AppUiState) { fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel: AppViewModel, uiState: AppUiState, focusRequester: FocusRequester) {
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current val navController = LocalNavController.current
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val snackbar = SnackbarController.current val snackbar = SnackbarController.current
val isRunningOnTv = remember { context.isRunningOnTv() }
val scrollState = rememberScrollState()
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
val isRunningOnTv = context.isRunningOnTv()
val settingsUiState by viewModel.uiState.collectAsStateWithLifecycle()
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
var currentText by remember { mutableStateOf("") }
var isBackgroundLocationGranted by remember { mutableStateOf(true) }
var showVpnPermissionDialog by remember { mutableStateOf(false) }
var showLocationServicesAlertDialog by remember { mutableStateOf(false) }
val didExportFiles by remember { mutableStateOf(false) }
var showAuthPrompt by remember { mutableStateOf(false) } var showAuthPrompt by remember { mutableStateOf(false) }
var showLocationDialog by remember { mutableStateOf(false) }
val screenPadding = 5.dp
val fillMaxWidth = .85f
LaunchedEffect(uiState.settings.trustedNetworkSSIDs) {
currentText = ""
}
val startForResult =
rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult(),
) { result: ActivityResult ->
if (result.resultCode == RESULT_OK) {
result.data
// Handle the Intent
}
viewModel.setBatteryOptimizeDisableShown()
}
val vpnActivityResultState =
rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult(),
onResult = {
val accepted = (it.resultCode == RESULT_OK)
if (accepted) {
viewModel.onToggleAutoTunnel(context)
} else {
showVpnPermissionDialog = true
}
},
)
fun isBatteryOptimizationsDisabled(): Boolean {
val pm = context.getSystemService(POWER_SERVICE) as PowerManager
return pm.isIgnoringBatteryOptimizations(context.packageName)
}
fun requestBatteryOptimizationsDisabled() {
val intent =
Intent().apply {
action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
data = Uri.parse("package:${context.packageName}")
}
startForResult.launch(intent)
}
fun handleAutoTunnelToggle() {
if (!uiState.generalState.isBatteryOptimizationDisableShown &&
!isBatteryOptimizationsDisabled() && !context.isRunningOnTv()
) {
return requestBatteryOptimizationsDisabled()
}
val intent = if (!uiState.settings.isKernelEnabled) {
VpnService.prepare(context)
} else {
null
}
if (intent != null) return vpnActivityResultState.launch(intent)
viewModel.onToggleAutoTunnel(context)
}
fun saveTrustedSSID() {
if (currentText.isNotEmpty()) {
viewModel.onSaveTrustedSSID(currentText)
}
}
fun checkFineLocationGranted() {
isBackgroundLocationGranted =
if (!fineLocationState.status.isGranted) {
false
} else {
viewModel.setLocationDisclosureShown()
true
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (
isRunningOnTv &&
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q
) {
checkFineLocationGranted()
} else {
val backgroundLocationState =
rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
isBackgroundLocationGranted =
if (!backgroundLocationState.status.isGranted) {
false
} else {
SideEffect { viewModel.setLocationDisclosureShown() }
true
}
}
}
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
checkFineLocationGranted()
}
if (!uiState.generalState.isLocationDisclosureShown) {
BackgroundLocationDisclosure(
onDismiss = { viewModel.setLocationDisclosureShown() },
onAttest = {
context.launchAppSettings()
viewModel.setLocationDisclosureShown()
},
scrollState,
focusRequester,
)
return
}
BackgroundLocationDialog(
showLocationDialog,
onDismiss = { showLocationDialog = false },
onAttest = { showLocationDialog = false },
)
LocationServicesDialog(
showLocationServicesAlertDialog,
onDismiss = { showVpnPermissionDialog = false },
onAttest = { handleAutoTunnelToggle() },
)
VpnDeniedDialog(showVpnPermissionDialog, onDismiss = { showVpnPermissionDialog = false })
if (showAuthPrompt) { if (showAuthPrompt) {
AuthorizationPrompt( AuthorizationPrompt(
onSuccess = { onSuccess = {
showAuthPrompt = false showAuthPrompt = false
viewModel.exportAllConfigs(context) viewModel.exportAllConfigs()
}, },
onError = { _ -> onError = { _ ->
showAuthPrompt = false showAuthPrompt = false
@@ -87,240 +257,351 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
) )
} }
fun onAutoTunnelWifiChecked() {
when (false) {
isBackgroundLocationGranted -> showLocationDialog = true
fineLocationState.status.isGranted -> showLocationDialog = true
viewModel.isLocationEnabled(context) ->
showLocationServicesAlertDialog = true
else -> {
viewModel.onToggleTunnelOnWifi()
}
}
}
Column( Column(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top), verticalArrangement = Arrangement.Top,
modifier = modifier =
Modifier Modifier
.verticalScroll(rememberScrollState())
.fillMaxSize() .fillMaxSize()
.padding(top = topPadding) .verticalScroll(scrollState)
.padding(bottom = 40.dp.scaledHeight()) .clickable(
.padding(horizontal = 24.dp.scaledWidth()) indication = null,
.then( interactionSource = interactionSource,
if (!isRunningOnTv) { ) {
Modifier.clickable( focusManager.clearFocus()
indication = null, },
interactionSource = interactionSource, ) {
) { Surface(
focusManager.clearFocus() tonalElevation = 2.dp,
} shadowElevation = 2.dp,
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface,
modifier =
(
if (isRunningOnTv) {
Modifier
.height(IntrinsicSize.Min)
.fillMaxWidth(fillMaxWidth)
.padding(top = 10.dp)
} else { } else {
Modifier Modifier
}, .fillMaxWidth(fillMaxWidth)
), .padding(top = 20.dp)
) {
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
Icons.Outlined.Bolt,
title = { Text(stringResource(R.string.auto_tunneling), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
description = {
Text(
stringResource(R.string.on_demand_rules),
style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline),
)
},
onClick = {
if (!uiState.generalState.isLocationDisclosureShown) return@SelectionItem navController.navigate(Route.LocationDisclosure)
navController.navigate(Route.AutoTunnel)
},
trailing = {
ForwardButton(Modifier.focusable()) { navController.navigate(Route.AutoTunnel) }
},
),
),
)
SurfaceSelectionGroupButton(
buildList {
add(
SelectionItem(
Icons.Filled.AppShortcut,
{
ScaledSwitch(
uiState.settings.isShortcutsEnabled,
onClick = { appViewModel.onToggleShortcutsEnabled() },
)
},
title = {
Text(
stringResource(R.string.enabled_app_shortcuts),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
onClick = { appViewModel.onToggleShortcutsEnabled() },
),
)
if (!isRunningOnTv) {
addAll(
listOf(
SelectionItem(
Icons.Outlined.VpnLock,
{
ScaledSwitch(
enabled = !(
(
uiState.settings.isTunnelOnWifiEnabled ||
uiState.settings.isTunnelOnEthernetEnabled ||
uiState.settings.isTunnelOnMobileDataEnabled
) &&
uiState.settings.isAutoTunnelEnabled
),
onClick = { appViewModel.onToggleAlwaysOnVPN() },
checked = uiState.settings.isAlwaysOnVpnEnabled,
)
},
title = {
Text(
stringResource(R.string.always_on_vpn_support),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
onClick = { appViewModel.onToggleAlwaysOnVPN() },
),
SelectionItem(
Icons.Outlined.AdminPanelSettings,
title = {
Text(
stringResource(R.string.kill_switch),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
onClick = {
context.launchVpnSettings()
},
trailing = {
ForwardButton { context.launchVpnSettings() }
},
),
),
)
} }
add(
SelectionItem(
Icons.Outlined.Restore,
{
ScaledSwitch(
uiState.settings.isRestoreOnBootEnabled,
onClick = { appViewModel.onToggleRestartAtBoot() },
)
},
title = {
Text(
stringResource(R.string.restart_at_boot),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
onClick = { appViewModel.onToggleRestartAtBoot() },
),
) )
}, .padding(bottom = 10.dp),
) ) {
Column(
SurfaceSelectionGroupButton( horizontalAlignment = Alignment.Start,
listOf( verticalArrangement = Arrangement.Top,
SelectionItem( modifier = Modifier.padding(15.dp),
Icons.AutoMirrored.Outlined.ViewQuilt, ) {
title = { Text(stringResource(R.string.appearance), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, SectionTitle(
onClick = { title = stringResource(id = R.string.auto_tunneling),
navController.navigate(Route.Appearance) padding = screenPadding,
)
ConfigurationToggle(
stringResource(id = R.string.tunnel_on_wifi),
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
checked = uiState.settings.isTunnelOnWifiEnabled,
padding = screenPadding,
onCheckChanged = { checked ->
if (!checked || settingsUiState.isRooted) viewModel.onToggleTunnelOnWifi().also { return@ConfigurationToggle }
onAutoTunnelWifiChecked()
}, },
trailing = { modifier =
ForwardButton { navController.navigate(Route.Appearance) } if (uiState.settings.isAutoTunnelEnabled) {
Modifier
} else {
Modifier
.focusRequester(focusRequester)
}, },
), )
SelectionItem( if (uiState.settings.isTunnelOnWifiEnabled) {
Icons.Outlined.Notifications, Column {
title = { Text(stringResource(R.string.notifications), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, FlowRow(
onClick = { modifier =
context.launchNotificationSettings() Modifier
}, .padding(screenPadding)
trailing = { .fillMaxWidth(),
ForwardButton { context.launchNotificationSettings() } horizontalArrangement = Arrangement.spacedBy(5.dp),
}, ) {
), uiState.settings.trustedNetworkSSIDs.forEach { ssid ->
SelectionItem( ClickableIconButton(
Icons.Outlined.Pin, onClick = {
title = { if (isRunningOnTv) {
Text( focusRequester.requestFocus()
stringResource(R.string.enable_app_lock), viewModel.onDeleteTrustedSSID(ssid)
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), }
) },
}, onIconClick = {
trailing = { if (isRunningOnTv) focusRequester.requestFocus()
ScaledSwitch( viewModel.onDeleteTrustedSSID(ssid)
uiState.generalState.isPinLockEnabled, },
onClick = { text = ssid,
if (uiState.generalState.isPinLockEnabled) { icon = Icons.Filled.Close,
appViewModel.onPinLockDisabled() )
} else { }
PinManager.initialize(context) if (uiState.settings.trustedNetworkSSIDs.isEmpty()) {
navController.navigate(Route.Lock) Text(
stringResource(R.string.none),
fontStyle = FontStyle.Italic,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface,
)
}
}
OutlinedTextField(
value = currentText,
onValueChange = { currentText = it },
label = { Text(stringResource(R.string.add_trusted_ssid)) },
modifier =
Modifier
.padding(
start = screenPadding,
top = 5.dp,
bottom = 10.dp,
),
supportingText = { WildcardSupportingLabel { context.openWebUrl(it) } },
maxLines = 1,
keyboardOptions =
KeyboardOptions(
capitalization = KeyboardCapitalization.None,
imeAction = ImeAction.Done,
),
keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }),
trailingIcon = {
if (currentText != "") {
IconButton(onClick = { saveTrustedSSID() }) {
Icon(
imageVector = Icons.Outlined.Add,
contentDescription =
if (currentText == "") {
stringResource(
id =
R.string
.trusted_ssid_empty_description,
)
} else {
stringResource(
id =
R.string
.trusted_ssid_value_description,
)
},
tint = MaterialTheme.colorScheme.primary,
)
}
} }
}, },
) )
}
}
ConfigurationToggle(
stringResource(R.string.tunnel_mobile_data),
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
checked = uiState.settings.isTunnelOnMobileDataEnabled,
padding = screenPadding,
onCheckChanged = { viewModel.onToggleTunnelOnMobileData() },
)
ConfigurationToggle(
stringResource(id = R.string.tunnel_on_ethernet),
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
checked = uiState.settings.isTunnelOnEthernetEnabled,
padding = screenPadding,
onCheckChanged = { viewModel.onToggleTunnelOnEthernet() },
)
ConfigurationToggle(
stringResource(R.string.restart_on_ping),
checked = uiState.settings.isPingEnabled,
padding = screenPadding,
onCheckChanged = { viewModel.onToggleRestartOnPing() },
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier =
(
if (!uiState.settings.isAutoTunnelEnabled) {
Modifier
} else {
Modifier.focusRequester(
focusRequester,
)
}
)
.fillMaxSize()
.padding(top = 5.dp),
horizontalArrangement = Arrangement.Center,
) {
TextButton(
onClick = {
if (uiState.tunnels.isEmpty()) return@TextButton context.showToast(R.string.tunnel_required)
handleAutoTunnelToggle()
},
) {
val autoTunnelButtonText =
if (uiState.settings.isAutoTunnelEnabled) {
stringResource(R.string.disable_auto_tunnel)
} else {
stringResource(id = R.string.enable_auto_tunnel)
}
Text(autoTunnelButtonText)
}
}
}
}
Surface(
tonalElevation = 2.dp,
shadowElevation = 2.dp,
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface,
modifier =
Modifier
.fillMaxWidth(fillMaxWidth)
.padding(vertical = 10.dp),
) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier = Modifier.padding(15.dp),
) {
SectionTitle(
title = stringResource(id = R.string.backend),
padding = screenPadding,
)
ConfigurationToggle(
stringResource(R.string.use_kernel),
enabled =
!(
uiState.settings.isAutoTunnelEnabled ||
uiState.settings.isAlwaysOnVpnEnabled ||
(uiState.vpnState.status == TunnelState.UP) ||
!settingsUiState.isKernelAvailable
),
checked = uiState.settings.isKernelEnabled,
padding = screenPadding,
onCheckChanged = {
viewModel.onToggleKernelMode()
}, },
onClick = { )
Row(
verticalAlignment = Alignment.CenterVertically,
modifier =
Modifier
.fillMaxSize()
.padding(top = 5.dp),
horizontalArrangement = Arrangement.Center,
) {
TextButton(
onClick = {
viewModel.onRequestRoot()
},
) {
Text(stringResource(R.string.request_root))
}
}
}
}
Surface(
tonalElevation = 2.dp,
shadowElevation = 2.dp,
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface,
modifier =
Modifier
.fillMaxWidth(fillMaxWidth)
.padding(vertical = 10.dp)
.padding(bottom = 10.dp),
) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier = Modifier.padding(15.dp),
) {
SectionTitle(
title = stringResource(id = R.string.other),
padding = screenPadding,
)
if (!isRunningOnTv) {
ConfigurationToggle(
stringResource(R.string.always_on_vpn_support),
enabled = !(
(
uiState.settings.isTunnelOnWifiEnabled ||
uiState.settings.isTunnelOnEthernetEnabled ||
uiState.settings.isTunnelOnMobileDataEnabled
) &&
uiState.settings.isAutoTunnelEnabled
),
checked = uiState.settings.isAlwaysOnVpnEnabled,
padding = screenPadding,
onCheckChanged = { viewModel.onToggleAlwaysOnVPN() },
)
ConfigurationToggle(
stringResource(R.string.enabled_app_shortcuts),
enabled = true,
checked = uiState.settings.isShortcutsEnabled,
padding = screenPadding,
onCheckChanged = { viewModel.onToggleShortcutsEnabled() },
)
}
ConfigurationToggle(
stringResource(R.string.restart_at_boot),
enabled = true,
checked = uiState.settings.isRestoreOnBootEnabled,
padding = screenPadding,
onCheckChanged = {
viewModel.onToggleRestartAtBoot()
},
)
ConfigurationToggle(
stringResource(R.string.enable_app_lock),
enabled = true,
checked = uiState.generalState.isPinLockEnabled,
padding = screenPadding,
onCheckChanged = {
if (uiState.generalState.isPinLockEnabled) { if (uiState.generalState.isPinLockEnabled) {
appViewModel.onPinLockDisabled() appViewModel.onPinLockDisabled()
} else { } else {
PinManager.initialize(context) // TODO may want to show a dialog before proceeding in the future
PinManager.initialize(WireGuardAutoTunnel.instance)
navController.navigate(Route.Lock) navController.navigate(Route.Lock)
} }
}, },
), )
), if (!isRunningOnTv) {
) Row(
verticalAlignment = Alignment.CenterVertically,
if (!isRunningOnTv) { modifier =
SurfaceSelectionGroupButton( Modifier
listOf( .fillMaxSize()
SelectionItem( .padding(top = 5.dp),
Icons.Outlined.Code, horizontalArrangement = Arrangement.Center,
title = { Text(stringResource(R.string.kernel), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) }, ) {
description = { TextButton(
Text( enabled = !didExportFiles,
stringResource(R.string.use_kernel), onClick = {
style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline), if (uiState.tunnels.isEmpty()) return@TextButton context.showToast(R.string.tunnel_required)
) showAuthPrompt = true
}, },
trailing = { ) {
ScaledSwitch( Text(stringResource(R.string.export_configs))
uiState.settings.isKernelEnabled, }
onClick = { appViewModel.onToggleKernelMode() }, }
enabled = !( }
uiState.settings.isAutoTunnelEnabled || }
uiState.settings.isAlwaysOnVpnEnabled ||
(uiState.vpnState.status == TunnelState.UP)
),
)
},
onClick = {
appViewModel.onToggleKernelMode()
},
),
),
)
}
if (!isRunningOnTv) {
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
Icons.Outlined.FolderZip,
title = {
Text(
stringResource(R.string.export_configs),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
onClick = {
if (uiState.tunnels.isEmpty()) return@SelectionItem context.showToast(R.string.tunnel_required)
showAuthPrompt = true
},
),
),
)
} }
} }
} }
@@ -0,0 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings
data class SettingsUiState(
val isRooted: Boolean = false,
val isKernelAvailable: Boolean = false,
)
@@ -1,36 +1,273 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings package com.zaneschepke.wireguardautotunnel.ui.screens.settings
import android.content.Context import android.content.Context
import androidx.core.content.FileProvider import android.location.LocationManager
import androidx.core.location.LocationManagerCompat
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.util.RootShell
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.FileUtils import com.zaneschepke.wireguardautotunnel.util.FileUtils
import com.zaneschepke.wireguardautotunnel.util.extensions.launchShareFile import com.zaneschepke.wireguardautotunnel.util.StringValue
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.time.Instant import kotlinx.coroutines.withContext
import java.io.File
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider
@HiltViewModel @HiltViewModel
class SettingsViewModel class SettingsViewModel
@Inject @Inject
constructor( constructor(
private val appDataRepository: AppDataRepository, private val appDataRepository: AppDataRepository,
private val rootShell: Provider<RootShell>,
private val fileUtils: FileUtils, private val fileUtils: FileUtils,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : ViewModel() { ) : ViewModel() {
fun exportAllConfigs(context: Context) = viewModelScope.launch { private val _uiState = MutableStateFlow(SettingsUiState())
val uiState = _uiState.onStart {
_uiState.update {
it.copy(isKernelAvailable = isKernelSupported(), isRooted = isRooted())
}
}.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
SettingsUiState(),
)
private val settings = appDataRepository.settings.getSettingsFlow()
.stateIn(viewModelScope, SharingStarted.Eagerly, Settings())
fun onSaveTrustedSSID(ssid: String) = viewModelScope.launch {
val trimmed = ssid.trim()
with(settings.value) {
if (!trustedNetworkSSIDs.contains(trimmed)) {
this.trustedNetworkSSIDs.add(ssid)
appDataRepository.settings.save(this)
} else {
SnackbarController.showMessage(
StringValue.StringResource(
R.string.error_ssid_exists,
),
)
}
}
}
fun setLocationDisclosureShown() = viewModelScope.launch {
appDataRepository.appState.setLocationDisclosureShown(true)
}
fun setBatteryOptimizeDisableShown() = viewModelScope.launch {
appDataRepository.appState.setBatteryOptimizationDisableShown(true)
}
fun onToggleTunnelOnMobileData() = viewModelScope.launch {
with(settings.value) {
appDataRepository.settings.save(
copy(
isTunnelOnMobileDataEnabled = !this.isTunnelOnMobileDataEnabled,
),
)
}
}
fun onDeleteTrustedSSID(ssid: String) = viewModelScope.launch {
with(settings.value) {
appDataRepository.settings.save(
copy(
trustedNetworkSSIDs = (this.trustedNetworkSSIDs - ssid).toMutableList(),
),
)
}
}
private fun exportTunnels(files: List<File>) = viewModelScope.launch {
fileUtils.saveFilesToZip(files).onSuccess {
SnackbarController.showMessage(StringValue.StringResource(R.string.exported_configs_message))
}.onFailure {
SnackbarController.showMessage(StringValue.StringResource(R.string.export_configs_failed))
}
}
fun onToggleAutoTunnel(context: Context) = viewModelScope.launch {
with(settings.value) {
var isAutoTunnelPaused = this.isAutoTunnelPaused
if (isAutoTunnelEnabled) {
ServiceManager.stopWatcherService(context)
} else {
ServiceManager.startWatcherService(context)
isAutoTunnelPaused = false
}
appDataRepository.settings.save(
copy(
isAutoTunnelEnabled = !isAutoTunnelEnabled,
isAutoTunnelPaused = isAutoTunnelPaused,
),
)
}
}
fun onToggleAlwaysOnVPN() = viewModelScope.launch {
with(settings.value) {
appDataRepository.settings.save(
copy(
isAlwaysOnVpnEnabled = !isAlwaysOnVpnEnabled,
),
)
}
}
fun onToggleTunnelOnEthernet() = viewModelScope.launch {
with(settings.value) {
appDataRepository.settings.save(
copy(
isTunnelOnEthernetEnabled = !isTunnelOnEthernetEnabled,
),
)
}
}
fun isLocationEnabled(context: Context): Boolean {
val locationManager =
context.getSystemService(
Context.LOCATION_SERVICE,
) as LocationManager
return LocationManagerCompat.isLocationEnabled(locationManager)
}
fun onToggleShortcutsEnabled() = viewModelScope.launch {
with(settings.value) {
appDataRepository.settings.save(
this.copy(
isShortcutsEnabled = !isShortcutsEnabled,
),
)
}
}
private fun saveKernelMode(enabled: Boolean) = viewModelScope.launch {
with(settings.value) {
appDataRepository.settings.save(
this.copy(
isKernelEnabled = enabled,
),
)
}
}
fun onToggleTunnelOnWifi() = viewModelScope.launch {
with(settings.value) {
appDataRepository.settings.save(
copy(
isTunnelOnWifiEnabled = !isTunnelOnWifiEnabled,
),
)
}
}
fun onToggleAmnezia() = viewModelScope.launch {
with(settings.value) {
if (isKernelEnabled) {
saveKernelMode(false)
}
appDataRepository.settings.save(
copy(
isAmneziaEnabled = !isAmneziaEnabled,
),
)
}
}
fun onToggleKernelMode() = viewModelScope.launch {
with(settings.value) {
if (!isKernelEnabled) {
requestRoot().onSuccess {
appDataRepository.settings.save(
copy(
isKernelEnabled = true,
isAmneziaEnabled = false,
),
)
}
} else {
saveKernelMode(enabled = false)
}
}
}
fun onToggleRestartOnPing() = viewModelScope.launch {
with(settings.value) {
appDataRepository.settings.save(
copy(
isPingEnabled = !isPingEnabled,
),
)
}
}
private suspend fun isKernelSupported(): Boolean {
return withContext(ioDispatcher) {
WgQuickBackend.hasKernelSupport()
}
}
fun onToggleRestartAtBoot() = viewModelScope.launch {
with(settings.value) {
appDataRepository.settings.save(
copy(
isRestoreOnBootEnabled = !isRestoreOnBootEnabled,
),
)
}
}
private suspend fun isRooted(): Boolean {
return try {
withContext(ioDispatcher) {
rootShell.get().start()
}
true
} catch (_: Exception) {
false
}
}
private suspend fun requestRoot(): Result<Unit> {
return withContext(ioDispatcher) {
kotlin.runCatching {
rootShell.get().start()
SnackbarController.showMessage(StringValue.StringResource(R.string.root_accepted))
}.onFailure {
SnackbarController.showMessage(StringValue.StringResource(R.string.error_root_denied))
}
}
}
fun onRequestRoot() = viewModelScope.launch {
requestRoot()
}
fun exportAllConfigs() = viewModelScope.launch {
kotlin.runCatching { kotlin.runCatching {
val shareFile = fileUtils.createNewShareFile("wg-export_${Instant.now().epochSecond}.zip")
val tunnels = appDataRepository.tunnels.getAll() val tunnels = appDataRepository.tunnels.getAll()
val wgFiles = fileUtils.createWgFiles(tunnels) val wgFiles = fileUtils.createWgFiles(tunnels)
val amFiles = fileUtils.createAmFiles(tunnels) val amFiles = fileUtils.createAmFiles(tunnels)
val allFiles = wgFiles + amFiles exportTunnels(wgFiles + amFiles)
fileUtils.zipAll(shareFile, allFiles)
val uri = FileProvider.getUriForFile(context, context.getString(R.string.provider), shareFile)
context.launchShareFile(uri)
} }
} }
} }
@@ -1,72 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Contrast
import androidx.compose.material.icons.outlined.Translate
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.ForwardButton
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@Composable
fun AppearanceScreen() {
val navController = LocalNavController.current
Scaffold(
topBar = {
TopNavBar(stringResource(R.string.appearance))
},
) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
modifier =
Modifier
.fillMaxSize().padding(it)
.padding(top = 24.dp.scaledHeight())
.padding(horizontal = 24.dp.scaledWidth()),
) {
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
Icons.Outlined.Translate,
title = { Text(stringResource(R.string.language), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
onClick = { navController.navigate(Route.Language) },
trailing = {
ForwardButton { navController.navigate(Route.Language) }
},
),
),
)
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
Icons.Outlined.Contrast,
title = { Text(stringResource(R.string.display_theme), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
onClick = { navController.navigate(Route.Display) },
trailing = {
ForwardButton { navController.navigate(Route.Display) }
},
),
),
)
}
}
}
@@ -1,63 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.display
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.common.button.IconSurfaceButton
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@Composable
fun DisplayScreen(appUiState: AppUiState, viewModel: DisplayViewModel = hiltViewModel()) {
Scaffold(
topBar = {
TopNavBar(stringResource(R.string.display_theme))
},
) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
modifier =
Modifier
.fillMaxSize()
.padding(it)
.padding(top = 24.dp.scaledHeight())
.padding(horizontal = 24.dp.scaledWidth()),
) {
IconSurfaceButton(
title = stringResource(R.string.automatic),
onClick = {
viewModel.onThemeChange(Theme.AUTOMATIC)
},
selected = appUiState.generalState.theme == Theme.AUTOMATIC,
)
IconSurfaceButton(
title = stringResource(R.string.light),
onClick = { viewModel.onThemeChange(Theme.LIGHT) },
selected = appUiState.generalState.theme == Theme.LIGHT,
)
IconSurfaceButton(
title = stringResource(R.string.dark),
onClick = { viewModel.onThemeChange(Theme.DARK) },
selected = appUiState.generalState.theme == Theme.DARK,
)
IconSurfaceButton(
title = stringResource(R.string.dynamic),
onClick = { viewModel.onThemeChange(Theme.DYNAMIC) },
selected = appUiState.generalState.theme == Theme.DYNAMIC,
)
}
}
}
@@ -1,21 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.display
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class DisplayViewModel
@Inject
constructor(
private val appStateRepository: AppStateRepository,
) : ViewModel() {
fun onThemeChange(theme: Theme) = viewModelScope.launch {
appStateRepository.setTheme(theme)
}
}
@@ -1,96 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.common.SelectedLabel
import com.zaneschepke.wireguardautotunnel.ui.common.button.SelectionItemButton
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
import java.text.Collator
import java.util.Locale
@Composable
fun LanguageScreen(appUiState: AppUiState, appViewModel: AppViewModel) {
val collator = Collator.getInstance(Locale.getDefault())
val locales = LocaleUtil.supportedLocales.map {
val tag = it.replace("_", "-")
Locale.forLanguageTag(tag)
}
val sortedLocales =
remember(locales) {
locales.sortedWith(compareBy(collator) { it.getDisplayName(it) }).toList()
}
Scaffold(
topBar = {
TopNavBar(stringResource(R.string.language))
},
) { padding ->
LazyColumn(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
modifier =
Modifier
.fillMaxSize().padding(padding)
.padding(horizontal = 24.dp.scaledWidth()).windowInsetsPadding(WindowInsets.navigationBars),
) {
item {
Box(modifier = Modifier.padding(top = 24.dp.scaledHeight())) {
SelectionItemButton(
buttonText = stringResource(R.string.automatic),
onClick = {
appViewModel.onLocaleChange(LocaleUtil.OPTION_PHONE_LANGUAGE)
},
trailing = {
if (appUiState.generalState.locale == LocaleUtil.OPTION_PHONE_LANGUAGE) {
SelectedLabel()
}
},
ripple = false,
)
}
}
items(sortedLocales, key = { it }) { locale ->
SelectionItemButton(
buttonText = locale.getDisplayLanguage(locale).replaceFirstChar { if (it.isLowerCase()) it.titlecase(locale) else it.toString() } +
if (locale.toLanguageTag().contains("-")) {
" (${locale.getDisplayCountry(locale)
.replaceFirstChar { if (it.isLowerCase()) it.titlecase(locale) else it.toString() }})"
} else {
""
},
onClick = {
appViewModel.onLocaleChange(locale.toLanguageTag())
},
trailing = {
if (locale.toLanguageTag() == appUiState.generalState.locale) {
SelectedLabel()
}
},
ripple = false,
)
}
}
}
}
@@ -1,330 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel
import android.Manifest
import android.os.Build
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Code
import androidx.compose.material.icons.outlined.Filter1
import androidx.compose.material.icons.outlined.NetworkPing
import androidx.compose.material.icons.outlined.Security
import androidx.compose.material.icons.outlined.SettingsEthernet
import androidx.compose.material.icons.outlined.SignalCellular4Bar
import androidx.compose.material.icons.outlined.Wifi
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.TrustedNetworkTextBox
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.WildcardsLabel
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackgroundLocationDialog
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LearnMoreLinkLabel
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LocationServicesDialog
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
import com.zaneschepke.wireguardautotunnel.util.extensions.isLocationServicesEnabled
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@OptIn(ExperimentalPermissionsApi::class, ExperimentalLayoutApi::class)
@Composable
fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltViewModel()) {
val context = LocalContext.current
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
var currentText by remember { mutableStateOf("") }
var isBackgroundLocationGranted by remember { mutableStateOf(true) }
var showLocationServicesAlertDialog by remember { mutableStateOf(false) }
var showLocationDialog by remember { mutableStateOf(false) }
fun checkFineLocationGranted() {
isBackgroundLocationGranted = fineLocationState.status.isGranted
}
fun onAutoTunnelWifiChecked() {
if (uiState.settings.isTunnelOnWifiEnabled) viewModel.onToggleTunnelOnWifi().also { return }
when (false) {
isBackgroundLocationGranted -> showLocationDialog = true
fineLocationState.status.isGranted -> showLocationDialog = true
context.isLocationServicesEnabled() ->
showLocationServicesAlertDialog = true
else -> {
viewModel.onToggleTunnelOnWifi()
}
}
}
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) checkFineLocationGranted()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (context.isRunningOnTv() && Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
checkFineLocationGranted()
} else {
val backgroundLocationState = rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
isBackgroundLocationGranted = backgroundLocationState.status.isGranted
}
}
LaunchedEffect(uiState.settings.trustedNetworkSSIDs) {
currentText = ""
}
LocationServicesDialog(
showLocationServicesAlertDialog,
onDismiss = { showLocationServicesAlertDialog = false },
onAttest = {
viewModel.onToggleTunnelOnWifi()
},
)
BackgroundLocationDialog(
showLocationDialog,
onDismiss = { showLocationDialog = false },
onAttest = { showLocationDialog = false },
)
Scaffold(
contentWindowInsets = WindowInsets(0.dp),
topBar = {
TopNavBar(stringResource(R.string.auto_tunneling))
},
) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
modifier =
Modifier
.fillMaxSize()
.padding(it)
.padding(top = 24.dp.scaledHeight())
.padding(horizontal = 24.dp.scaledWidth()),
) {
SurfaceSelectionGroupButton(
buildList {
addAll(
listOf(
SelectionItem(
Icons.Outlined.Wifi,
title = {
Text(
stringResource(R.string.tunnel_on_wifi),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
description = {
},
trailing = {
ScaledSwitch(
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
checked = uiState.settings.isTunnelOnWifiEnabled,
onClick = {
if (uiState.settings.isWifiNameByShellEnabled) viewModel.onToggleTunnelOnWifi().also { return@ScaledSwitch }
onAutoTunnelWifiChecked()
},
)
},
onClick = {
if (uiState.settings.isWifiNameByShellEnabled) viewModel.onToggleTunnelOnWifi().also { return@SelectionItem }
onAutoTunnelWifiChecked()
},
),
SelectionItem(
Icons.Outlined.Code,
title = {
Text(
stringResource(R.string.wifi_name_via_shell),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
description = {
Text(
stringResource(R.string.use_root_shell_for_wifi),
style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline),
)
},
trailing = {
ScaledSwitch(
checked = uiState.settings.isWifiNameByShellEnabled,
onClick = {
viewModel.onRootShellWifiToggle()
},
)
},
onClick = {
viewModel.onRootShellWifiToggle()
},
),
),
)
if (uiState.settings.isTunnelOnWifiEnabled) {
addAll(
listOf(
SelectionItem(
Icons.Outlined.Filter1,
title = {
Text(
stringResource(R.string.use_wildcards),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
description = {
LearnMoreLinkLabel({ context.openWebUrl(it) }, stringResource(id = R.string.docs_wildcards))
},
trailing = {
ScaledSwitch(
checked = uiState.settings.isWildcardsEnabled,
onClick = {
viewModel.onToggleWildcards()
},
)
},
onClick = {
viewModel.onToggleWildcards()
},
),
SelectionItem(
title = {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp.scaledHeight()),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.weight(4f, false)
.fillMaxWidth(),
) {
val icon = Icons.Outlined.Security
Icon(
icon,
icon.name,
modifier = Modifier.size(iconSize),
)
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically),
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp.scaledWidth())
.padding(vertical = 6.dp.scaledHeight()),
) {
Text(
stringResource(R.string.trusted_wifi_names),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
}
}
}
},
description = {
TrustedNetworkTextBox(
uiState.settings.trustedNetworkSSIDs,
onDelete = viewModel::onDeleteTrustedSSID,
currentText = currentText,
onSave = viewModel::onSaveTrustedSSID,
onValueChange = { currentText = it },
supporting = {
if (uiState.settings.isWildcardsEnabled) {
WildcardsLabel()
}
},
)
},
),
),
)
}
},
)
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
Icons.Outlined.SignalCellular4Bar,
title = {
Text(
stringResource(R.string.tunnel_mobile_data),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ScaledSwitch(
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
checked = uiState.settings.isTunnelOnMobileDataEnabled,
onClick = { viewModel.onToggleTunnelOnMobileData() },
)
},
onClick = {
viewModel.onToggleTunnelOnMobileData()
},
),
SelectionItem(
Icons.Outlined.SettingsEthernet,
title = {
Text(
stringResource(R.string.tunnel_on_ethernet),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ScaledSwitch(
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
checked = uiState.settings.isTunnelOnEthernetEnabled,
onClick = { viewModel.onToggleTunnelOnEthernet() },
)
},
onClick = {
viewModel.onToggleTunnelOnEthernet()
},
),
SelectionItem(
Icons.Outlined.NetworkPing,
title = {
Text(
stringResource(R.string.restart_on_ping),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ScaledSwitch(
checked = uiState.settings.isPingEnabled,
onClick = { viewModel.onToggleRestartOnPing() },
)
},
onClick = {
viewModel.onToggleRestartOnPing()
},
),
),
)
}
}
}
@@ -1,131 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wireguard.android.util.RootShell
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.AppShell
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.util.StringValue
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Provider
@HiltViewModel
class AutoTunnelViewModel
@Inject
constructor(
private val appDataRepository: AppDataRepository,
@AppShell private val rootShell: Provider<RootShell>,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : ViewModel() {
private val settings = appDataRepository.settings.getSettingsFlow()
.stateIn(viewModelScope, SharingStarted.Eagerly, Settings())
fun onToggleTunnelOnWifi() = viewModelScope.launch {
with(settings.value) {
appDataRepository.settings.save(
copy(
isTunnelOnWifiEnabled = !isTunnelOnWifiEnabled,
),
)
}
}
fun onToggleTunnelOnMobileData() = viewModelScope.launch {
with(settings.value) {
appDataRepository.settings.save(
copy(
isTunnelOnMobileDataEnabled = !isTunnelOnMobileDataEnabled,
),
)
}
}
fun onToggleWildcards() = viewModelScope.launch {
with(settings.value) {
appDataRepository.settings.save(
copy(
isWildcardsEnabled = !isWildcardsEnabled,
),
)
}
}
fun onDeleteTrustedSSID(ssid: String) = viewModelScope.launch {
with(settings.value) {
appDataRepository.settings.save(
copy(
trustedNetworkSSIDs = (trustedNetworkSSIDs - ssid).toMutableList(),
),
)
}
}
fun onRootShellWifiToggle() = viewModelScope.launch {
requestRoot().onSuccess {
with(settings.value) {
appDataRepository.settings.save(
copy(isWifiNameByShellEnabled = !isWifiNameByShellEnabled),
)
}
}
}
private suspend fun requestRoot(): Result<Unit> {
return withContext(ioDispatcher) {
kotlin.runCatching {
rootShell.get().start()
SnackbarController.showMessage(StringValue.StringResource(R.string.root_accepted))
}.onFailure {
SnackbarController.showMessage(StringValue.StringResource(R.string.error_root_denied))
}
}
}
fun onToggleTunnelOnEthernet() = viewModelScope.launch {
with(settings.value) {
appDataRepository.settings.save(
copy(
isTunnelOnEthernetEnabled = !isTunnelOnEthernetEnabled,
),
)
}
}
fun onSaveTrustedSSID(ssid: String) = viewModelScope.launch {
if (ssid.isEmpty()) return@launch
val trimmed = ssid.trim()
with(settings.value) {
if (!trustedNetworkSSIDs.contains(trimmed)) {
this.trustedNetworkSSIDs.add(ssid)
appDataRepository.settings.save(this)
} else {
SnackbarController.showMessage(
StringValue.StringResource(
R.string.error_ssid_exists,
),
)
}
}
}
fun onToggleRestartOnPing() = viewModelScope.launch {
with(settings.value) {
appDataRepository.settings.save(
copy(
isPingEnabled = !isPingEnabled,
),
)
}
}
}
@@ -1,107 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.CustomTextField
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun TrustedNetworkTextBox(
trustedNetworks: List<String>,
onDelete: (ssid: String) -> Unit,
currentText: String,
onSave: (ssid: String) -> Unit,
onValueChange: (network: String) -> Unit,
supporting: @Composable () -> Unit,
) {
val context = LocalContext.current
Column(verticalArrangement = Arrangement.spacedBy(10.dp.scaledHeight())) {
FlowRow(
modifier =
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.CenterHorizontally),
) {
trustedNetworks.forEach { ssid ->
ClickableIconButton(
onClick = {
if (context.isRunningOnTv()) {
onDelete(ssid)
}
},
onIconClick = {
onDelete(ssid)
},
text = ssid,
icon = Icons.Filled.Close,
)
}
}
CustomTextField(
textStyle = MaterialTheme.typography.bodySmall,
value = currentText,
onValueChange = onValueChange,
interactionSource = remember { MutableInteractionSource() },
label = { Text(stringResource(R.string.add_wifi_name)) },
containerColor = MaterialTheme.colorScheme.surface,
supportingText = supporting,
modifier =
Modifier
.padding(
top = 5.dp,
bottom = 10.dp,
).fillMaxWidth().padding(end = 16.dp.scaledWidth()),
singleLine = true,
keyboardOptions =
KeyboardOptions(
capitalization = KeyboardCapitalization.None,
imeAction = ImeAction.Done,
),
keyboardActions = KeyboardActions(onDone = { onSave(currentText) }),
trailing = {
if (currentText != "") {
IconButton(onClick = {
onSave(currentText)
}) {
val icon = Icons.Outlined.Add
Icon(
imageVector = icon,
contentDescription = stringResource(
R.string
.trusted_ssid_value_description,
),
tint = MaterialTheme.colorScheme.primary,
)
}
}
},
)
}
}
@@ -1,16 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import com.zaneschepke.wireguardautotunnel.R
@Composable
fun WildcardsLabel() {
Text(
stringResource(R.string.wildcards_active),
style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline, fontStyle = FontStyle.Italic),
)
}
@@ -0,0 +1,88 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.LocationOff
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
@Composable
fun BackgroundLocationDisclosure(onDismiss: () -> Unit, onAttest: () -> Unit, scrollState: ScrollState, focusRequester: FocusRequester) {
val context = LocalContext.current
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
modifier =
Modifier
.fillMaxSize()
.verticalScroll(scrollState),
) {
Icon(
Icons.Rounded.LocationOff,
contentDescription = stringResource(id = R.string.map),
modifier =
Modifier
.padding(30.dp)
.size(128.dp),
)
Text(
stringResource(R.string.prominent_background_location_title),
textAlign = TextAlign.Center,
modifier = Modifier.padding(30.dp),
fontSize = 20.sp,
)
Text(
stringResource(R.string.prominent_background_location_message),
textAlign = TextAlign.Center,
modifier = Modifier.padding(30.dp),
fontSize = 15.sp,
)
Row(
modifier =
if (context.isRunningOnTv()) {
Modifier
.fillMaxWidth()
.padding(10.dp)
} else {
Modifier
.fillMaxWidth()
.padding(30.dp)
},
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly,
) {
TextButton(onClick = { onDismiss() }) {
Text(stringResource(id = R.string.no_thanks))
}
TextButton(
modifier = Modifier.focusRequester(focusRequester),
onClick = {
onAttest()
},
) {
Text(stringResource(id = R.string.turn_on))
}
}
}
}
@@ -1,22 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowForward
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
@Composable
fun ForwardButton(modifier: Modifier = Modifier.focusable(), onClick: () -> Unit) {
IconButton(
modifier = modifier,
onClick = onClick,
) {
val icon = Icons.AutoMirrored.Outlined.ArrowForward
Icon(icon, icon.name, Modifier.size(iconSize))
}
}
@@ -12,18 +12,18 @@ import androidx.compose.ui.text.withStyle
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
@Composable @Composable
fun LearnMoreLinkLabel(onClick: (url: String) -> Unit, url: String) { fun WildcardSupportingLabel(onClick: (url: String) -> Unit) {
// TODO update link when docs are fully updated // TODO update link when docs are fully updated
val gettingStarted = val gettingStarted =
buildAnnotatedString { buildAnnotatedString {
pushStringAnnotation( pushStringAnnotation(
tag = "details", tag = "details",
annotation = url, annotation = stringResource(id = R.string.docs_wildcards),
) )
withStyle( withStyle(
style = SpanStyle(color = MaterialTheme.colorScheme.primary), style = SpanStyle(color = MaterialTheme.colorScheme.primary),
) { ) {
append(stringResource(id = R.string.learn_more)) append(stringResource(id = R.string.wildcard_supported))
} }
pop() pop()
} }
@@ -1,104 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.disclosure
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.LocationOn
import androidx.compose.material.icons.rounded.PermScanWifi
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.theme.topPadding
import com.zaneschepke.wireguardautotunnel.util.extensions.goFromRoot
import com.zaneschepke.wireguardautotunnel.util.extensions.launchAppSettings
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@Composable
fun LocationDisclosureScreen(appViewModel: AppViewModel, appUiState: AppUiState) {
val context = LocalContext.current
val navController = LocalNavController.current
LaunchedEffect(Unit, appUiState) {
if (appUiState.generalState.isLocationDisclosureShown) navController.goFromRoot(Route.AutoTunnel)
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
modifier =
Modifier.fillMaxSize().padding(top = topPadding).padding(horizontal = 24.dp.scaledWidth()),
) {
val icon = Icons.Rounded.PermScanWifi
Icon(
icon,
contentDescription = icon.name,
modifier = Modifier
.padding(30.dp.scaledHeight())
.size(128.dp.scaledHeight()),
)
Text(
stringResource(R.string.prominent_background_location_title),
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold),
)
Text(
stringResource(R.string.prominent_background_location_message),
style = MaterialTheme.typography.bodyLarge,
)
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
Icons.Outlined.LocationOn,
title = {
Text(
stringResource(R.string.launch_app_settings),
style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface),
)
},
onClick = {
context.launchAppSettings().also {
appViewModel.setLocationDisclosureShown()
}
},
trailing = {
ForwardButton {
context.launchAppSettings().also {
appViewModel.setLocationDisclosureShown()
}
}
},
),
),
)
SurfaceSelectionGroupButton(
listOf(
SelectionItem(
title = { Text(stringResource(R.string.skip), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) },
onClick = { appViewModel.setLocationDisclosureShown() },
trailing = {
ForwardButton { appViewModel.setLocationDisclosureShown() }
},
),
),
)
}
}
@@ -1,250 +1,310 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support package com.zaneschepke.wireguardautotunnel.ui.screens.support
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
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.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
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.filled.AttachMoney import androidx.compose.material.icons.automirrored.rounded.ArrowForward
import androidx.compose.material.icons.filled.Book import androidx.compose.material.icons.rounded.Book
import androidx.compose.material.icons.filled.Mail import androidx.compose.material.icons.rounded.FormatListNumbered
import androidx.compose.material.icons.filled.Policy import androidx.compose.material.icons.rounded.Mail
import androidx.compose.material.icons.filled.ViewTimeline import androidx.compose.material3.HorizontalDivider
import androidx.compose.material.icons.outlined.ViewHeadline import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
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.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.zaneschepke.wireguardautotunnel.BuildConfig import com.zaneschepke.wireguardautotunnel.BuildConfig
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.AppUiState import com.zaneschepke.wireguardautotunnel.ui.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.Route import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.label.VersionLabel
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.theme.topPadding
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.launchSupportEmail import com.zaneschepke.wireguardautotunnel.util.extensions.launchSupportEmail
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@Composable @Composable
fun SupportScreen(appUiState: AppUiState, appViewModel: AppViewModel) { fun SupportScreen(focusRequester: FocusRequester, appUiState: AppUiState) {
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current val navController = LocalNavController.current
val fillMaxWidth = .85f
var showDialog by remember { mutableStateOf(false) }
if (showDialog) {
InfoDialog(onAttest = {
showDialog = false
appViewModel.onToggleLocalLogging()
}, onDismiss = {
showDialog = false
}, title = {
Text(stringResource(R.string.configuration_change))
}, body = { Text(stringResource(R.string.requires_app_relaunch)) }, confirmText = { Text(stringResource(R.string.yes)) })
}
Column( Column(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top), verticalArrangement = Arrangement.Top,
modifier = modifier =
Modifier Modifier
.fillMaxSize() .fillMaxSize()
.padding(top = topPadding)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(horizontal = 24.dp.scaledWidth()), .focusable(),
) { ) {
GroupLabel(stringResource(R.string.thank_you)) Surface(
SurfaceSelectionGroupButton( tonalElevation = 2.dp,
buildList { shadowElevation = 2.dp,
add( shape = RoundedCornerShape(12.dp),
SelectionItem( color = MaterialTheme.colorScheme.surface,
Icons.Filled.Book, modifier =
title = { (
Text( if (context.isRunningOnTv()) {
stringResource(R.string.docs_description), Modifier
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), .height(IntrinsicSize.Min)
) .fillMaxWidth(fillMaxWidth)
}, .padding(top = 10.dp)
trailing = { } else {
ForwardButton { context.openWebUrl(context.getString(R.string.docs_url)) } Modifier
}, .fillMaxWidth(fillMaxWidth)
onClick = { .padding(top = 20.dp)
context.openWebUrl(context.getString(R.string.docs_url)) }
},
),
) )
if (!context.isRunningOnTv()) { .padding(bottom = 25.dp),
add( ) {
SelectionItem( Column(modifier = Modifier.padding(20.dp)) {
Icons.Outlined.ViewHeadline, val forwardIcon = Icons.AutoMirrored.Rounded.ArrowForward
title = { Text(
Text( stringResource(R.string.thank_you),
stringResource(R.string.local_logging), textAlign = TextAlign.Start,
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), fontWeight = FontWeight.Bold,
) modifier = Modifier.padding(bottom = 20.dp),
}, fontSize = 16.sp,
description = { )
Text( Text(
stringResource(R.string.enable_local_logging), stringResource(id = R.string.support_help_text),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.outline), textAlign = TextAlign.Start,
) fontSize = 16.sp,
}, modifier = Modifier.padding(bottom = 20.dp),
trailing = { )
ScaledSwitch( TextButton(
appUiState.generalState.isLocalLogsEnabled, onClick = {
onClick = { context.openWebUrl(
showDialog = true context.resources.getString(R.string.docs_url),
}, )
) },
}, modifier =
onClick = { Modifier
showDialog = true .padding(vertical = 5.dp)
}, .focusRequester(focusRequester),
), ) {
) Row(
if (appUiState.generalState.isLocalLogsEnabled) { horizontalArrangement = Arrangement.SpaceBetween,
add( verticalAlignment = Alignment.CenterVertically,
SelectionItem( modifier = Modifier.fillMaxWidth(),
Icons.Filled.ViewTimeline, ) {
title = { Row {
Text( val icon = Icons.Rounded.Book
stringResource(R.string.read_logs), Icon(icon, icon.name)
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), Text(
) stringResource(id = R.string.docs_description),
}, textAlign = TextAlign.Justify,
trailing = { modifier =
ForwardButton { Modifier
navController.navigate(Route.Logs) .padding(start = 10.dp)
} .weight(
}, weight = 1.0f,
onClick = { fill = false,
navController.navigate(Route.Logs) ),
}, softWrap = true,
), )
}
Icon(
forwardIcon,
forwardIcon.name,
) )
} }
} }
add( HorizontalDivider(
SelectionItem( thickness = 0.5.dp,
Icons.Filled.Policy, color = MaterialTheme.colorScheme.onBackground,
title = { )
Text( TextButton(
stringResource(R.string.privacy_policy), onClick = {
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface), context.openWebUrl(
context.resources.getString(R.string.telegram_url),
)
},
modifier = Modifier.padding(vertical = 5.dp),
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(),
) {
Row {
val icon = ImageVector.vectorResource(R.drawable.telegram)
Icon(
icon,
icon.name,
Modifier.size(25.dp),
) )
}, Text(
trailing = { stringResource(id = R.string.chat_description),
ForwardButton { context.openWebUrl(context.getString(R.string.privacy_policy_url)) } textAlign = TextAlign.Justify,
}, modifier = Modifier.padding(start = 10.dp),
onClick = { )
context.openWebUrl(context.getString(R.string.privacy_policy_url)) }
}, Icon(
), forwardIcon,
) forwardIcon.name,
}, )
) }
SurfaceSelectionGroupButton(
buildList {
addAll(
listOf(
SelectionItem(
ImageVector.vectorResource(R.drawable.telegram),
title = {
Text(
stringResource(R.string.chat_description),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ForwardButton {
context.openWebUrl(context.getString(R.string.telegram_url))
}
},
onClick = {
context.openWebUrl(context.getString(R.string.telegram_url))
},
),
SelectionItem(
ImageVector.vectorResource(R.drawable.github),
title = {
Text(
stringResource(R.string.open_issue),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ForwardButton {
context.openWebUrl(context.getString(R.string.github_url))
}
},
onClick = {
context.openWebUrl(context.getString(R.string.github_url))
},
),
SelectionItem(
Icons.Filled.Mail,
title = {
Text(
stringResource(R.string.email_description),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ForwardButton {
context.launchSupportEmail()
}
},
onClick = {
context.launchSupportEmail()
},
),
),
)
if (BuildConfig.FLAVOR == "fdroid") {
add(
SelectionItem(
Icons.Filled.AttachMoney,
title = {
Text(
stringResource(R.string.donate),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ForwardButton {
context.openWebUrl(context.getString(R.string.donate_url))
}
},
onClick = {
context.openWebUrl(context.getString(R.string.donate_url))
},
),
)
} }
HorizontalDivider(
thickness = 0.5.dp,
color = MaterialTheme.colorScheme.onBackground,
)
TextButton(
onClick = {
context.openWebUrl(
context.resources.getString(R.string.github_url),
)
},
modifier = Modifier.padding(vertical = 5.dp),
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(),
) {
Row {
val icon = ImageVector.vectorResource(R.drawable.github)
Icon(
imageVector = icon,
icon.name,
Modifier.size(25.dp),
)
Text(
stringResource(id = R.string.open_issue),
textAlign = TextAlign.Justify,
modifier = Modifier.padding(start = 10.dp),
)
}
Icon(
forwardIcon,
forwardIcon.name,
)
}
}
HorizontalDivider(
thickness = 0.5.dp,
color = MaterialTheme.colorScheme.onBackground,
)
TextButton(
onClick = { context.launchSupportEmail() },
modifier = Modifier.padding(vertical = 5.dp),
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(),
) {
Row {
val icon = Icons.Rounded.Mail
Icon(icon, icon.name)
Text(
stringResource(id = R.string.email_description),
textAlign = TextAlign.Justify,
modifier = Modifier.padding(start = 10.dp),
)
}
Icon(
forwardIcon,
forwardIcon.name,
)
}
}
if (!context.isRunningOnTv()) {
HorizontalDivider(
thickness = 0.5.dp,
color = MaterialTheme.colorScheme.onBackground,
)
TextButton(
onClick = { navController.navigate(Route.Logs) },
modifier = Modifier.padding(vertical = 5.dp),
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(),
) {
Row {
val icon = Icons.Rounded.FormatListNumbered
Icon(icon, icon.name)
Text(
stringResource(id = R.string.read_logs),
textAlign = TextAlign.Justify,
modifier = Modifier.padding(start = 10.dp),
)
}
Icon(
Icons.AutoMirrored.Rounded.ArrowForward,
stringResource(id = R.string.go),
)
}
}
}
}
}
Spacer(modifier = Modifier.weight(1f))
Text(
stringResource(id = R.string.privacy_policy),
style = TextStyle(textDecoration = TextDecoration.Underline),
fontSize = 16.sp,
modifier =
Modifier.clickable {
context.openWebUrl(
context.resources.getString(R.string.privacy_policy_url),
)
}, },
) )
VersionLabel() Row(
horizontalArrangement = Arrangement.spacedBy(25.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(25.dp),
) {
val version =
buildAnnotatedString {
append(stringResource(id = R.string.version))
append(": ")
append(BuildConfig.VERSION_NAME)
}
val mode =
buildAnnotatedString {
append(stringResource(R.string.mode))
append(": ")
when (appUiState.settings.isKernelEnabled) {
true -> append(stringResource(id = R.string.kernel))
false -> append(stringResource(id = R.string.userspace))
}
}
Text(version.text, modifier = Modifier.focusable())
Text(mode.text)
}
} }
} }
@@ -1,5 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support.logs package com.zaneschepke.wireguardautotunnel.ui.screens.support.logs
import android.annotation.SuppressLint
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -31,17 +32,15 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ClipboardManager import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.zaneschepke.logcatter.model.LogMessage import com.zaneschepke.logcatter.model.LogMessage
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
import com.zaneschepke.wireguardautotunnel.ui.common.text.LogTypeLabel import com.zaneschepke.wireguardautotunnel.ui.common.text.LogTypeLabel
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable @Composable
fun LogsScreen(viewModel: LogsViewModel = hiltViewModel()) { fun LogsScreen(viewModel: LogsViewModel = hiltViewModel()) {
val logs = viewModel.logs val logs = viewModel.logs
@@ -84,9 +83,6 @@ fun LogsScreen(viewModel: LogsViewModel = hiltViewModel()) {
} }
Scaffold( Scaffold(
topBar = {
TopNavBar(stringResource(R.string.logs))
},
floatingActionButton = { floatingActionButton = {
FloatingActionButton( FloatingActionButton(
onClick = { onClick = {
@@ -111,7 +107,7 @@ fun LogsScreen(viewModel: LogsViewModel = hiltViewModel()) {
modifier = modifier =
Modifier Modifier
.fillMaxSize() .fillMaxSize()
.padding(horizontal = 24.dp).padding(it), .padding(horizontal = 24.dp),
) { ) {
itemsIndexed( itemsIndexed(
logs, logs,
@@ -11,7 +11,6 @@ import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.module.MainDispatcher import com.zaneschepke.wireguardautotunnel.module.MainDispatcher
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.FileUtils
import com.zaneschepke.wireguardautotunnel.util.extensions.chunked import com.zaneschepke.wireguardautotunnel.util.extensions.chunked
import com.zaneschepke.wireguardautotunnel.util.extensions.launchShareFile import com.zaneschepke.wireguardautotunnel.util.extensions.launchShareFile
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
@@ -20,6 +19,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import java.io.File
import java.time.Duration import java.time.Duration
import java.time.Instant import java.time.Instant
import javax.inject.Inject import javax.inject.Inject
@@ -29,7 +29,6 @@ class LogsViewModel
@Inject @Inject
constructor( constructor(
private val localLogCollector: LogReader, private val localLogCollector: LogReader,
private val fileUtils: FileUtils,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher, @IoDispatcher private val ioDispatcher: CoroutineDispatcher,
@MainDispatcher private val mainDispatcher: CoroutineDispatcher, @MainDispatcher private val mainDispatcher: CoroutineDispatcher,
) : ViewModel() { ) : ViewModel() {
@@ -52,7 +51,12 @@ constructor(
fun shareLogs(context: Context): Job = viewModelScope.launch(ioDispatcher) { fun shareLogs(context: Context): Job = viewModelScope.launch(ioDispatcher) {
runCatching { runCatching {
val file = fileUtils.createNewShareFile("${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.zip") val sharePath = File(context.filesDir, "external_files")
if (sharePath.exists()) sharePath.delete()
sharePath.mkdir()
val file = File("${sharePath.path + "/" + Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.zip")
if (file.exists()) file.delete()
file.createNewFile()
localLogCollector.zipLogFiles(file.absolutePath) localLogCollector.zipLogFiles(file.absolutePath)
val uri = FileProvider.getUriForFile(context, context.getString(R.string.provider), file) val uri = FileProvider.getUriForFile(context, context.getString(R.string.provider), file)
context.launchShareFile(uri) context.launchShareFile(uri)
@@ -3,14 +3,14 @@ package com.zaneschepke.wireguardautotunnel.ui.theme
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
val OffWhite = Color(0xFFE5E1E5) val OffWhite = Color(0xFFE5E1E5)
val LightGrey = Color(0xFFCAC4D0) val LightGrey = Color(0xFF8D9D9F)
val Aqua = Color(0xFF76BEBD) val Aqua = Color(0xFF76BEBD)
val SilverTree = Color(0xFF6DB58B) val SilverTree = Color(0xFF6DB58B)
val Plantation = Color(0xFF264A49) val Plantation = Color(0xFF264A49)
val Shark = Color(0xFF21272A) val Shark = Color(0xFF21272A)
val BalticSea = Color(0xFF1C1B1F) val BalticSea = Color(0xFF1C1B1F)
val Brick = Color(0xFFCE4257) val Brick = Color(0xFFCE4257)
val Straw = Color(0xFFD4C483) val Corn = Color(0xFFFBEC5D)
sealed class ThemeColors( sealed class ThemeColors(
val background: Color, val background: Color,
@@ -19,7 +19,7 @@ sealed class ThemeColors(
val secondary: Color, val secondary: Color,
val onSurface: Color, val onSurface: Color,
) { ) {
// TODO fix light theme colors
data object Light : ThemeColors( data object Light : ThemeColors(
background = LightGrey, background = LightGrey,
surface = OffWhite, surface = OffWhite,
@@ -1,7 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.theme
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
val iconSize = 24.dp.scaledHeight()
val topPadding = 80.dp.scaledHeight()
@@ -36,39 +36,24 @@ private val LightColorScheme =
onSecondaryContainer = ThemeColors.Light.primary, onSecondaryContainer = ThemeColors.Light.primary,
) )
enum class Theme {
AUTOMATIC,
LIGHT,
DARK,
DYNAMIC,
}
@Composable @Composable
fun WireguardAutoTunnelTheme(theme: Theme = Theme.AUTOMATIC, content: @Composable () -> Unit) { fun WireguardAutoTunnelTheme(
// force dark theme
useDarkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit,
) {
val context = LocalContext.current val context = LocalContext.current
var isDark = isSystemInDarkTheme() val colorScheme = when {
val autoTheme = if (isDark) DarkColorScheme else LightColorScheme (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) -> {
val colorScheme = when (theme) { if (useDarkTheme) {
Theme.AUTOMATIC -> autoTheme dynamicDarkColorScheme(context)
Theme.DARK -> {
isDark = true
DarkColorScheme
}
Theme.LIGHT -> {
isDark = false
LightColorScheme
}
Theme.DYNAMIC -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (isDark) {
dynamicDarkColorScheme(context)
} else {
dynamicLightColorScheme(context)
}
} else { } else {
autoTheme dynamicLightColorScheme(context)
} }
} }
useDarkTheme -> DarkColorScheme
// TODO force dark theme for now until light theme designed
else -> DarkColorScheme
} }
val view = LocalView.current val view = LocalView.current
if (!view.isInEditMode) { if (!view.isInEditMode) {
@@ -77,7 +62,8 @@ fun WireguardAutoTunnelTheme(theme: Theme = Theme.AUTOMATIC, content: @Composabl
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
window.statusBarColor = Color.Transparent.toArgb() window.statusBarColor = Color.Transparent.toArgb()
window.navigationBarColor = Color.Transparent.toArgb() window.navigationBarColor = Color.Transparent.toArgb()
WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars = !isDark WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars =
!useDarkTheme
} }
} }
@@ -5,9 +5,9 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.util.extensions.scaled
// Set of Material typography styles to start with // Set of Material typography styles to start with
@@ -17,47 +17,43 @@ val inter = FontFamily(
val Typography = val Typography =
Typography( Typography(
bodyLarge = TextStyle( bodyLarge =
TextStyle(
fontFamily = inter, fontFamily = inter,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
fontSize = 16.sp.scaled(), fontSize = 16.sp,
lineHeight = 24.sp.scaled(), lineHeight = 24.sp,
letterSpacing = 0.5.sp, letterSpacing = 0.5.sp,
), ),
bodySmall = TextStyle( bodySmall = TextStyle(
fontFamily = inter, fontFamily = inter,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
fontSize = 12.sp.scaled(), fontSize = 13.sp,
lineHeight = 20.sp.scaled(), lineHeight = 20.sp,
letterSpacing = 1.sp, letterSpacing = 1.sp,
color = LightGrey, color = LightGrey,
), ),
bodyMedium = TextStyle(
fontFamily = inter,
fontSize = 14.sp.scaled(),
lineHeight = 20.sp.scaled(),
fontWeight = FontWeight(400),
letterSpacing = 0.25.sp,
),
labelLarge = TextStyle( labelLarge = TextStyle(
fontFamily = inter, fontFamily = inter,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
fontSize = 15.sp.scaled(), fontSize = 15.sp,
lineHeight = 18.sp.scaled(), lineHeight = 18.sp,
letterSpacing = 0.sp, letterSpacing = 0.sp,
), ),
labelMedium = TextStyle( labelMedium = TextStyle(
fontFamily = inter, fontFamily = inter,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
fontSize = 12.sp.scaled(), fontSize = 12.sp,
lineHeight = 16.sp.scaled(), lineHeight = 16.sp,
letterSpacing = 0.5.sp, letterSpacing = 0.5.sp,
), ),
titleMedium = TextStyle( titleMedium = TextStyle(
fontFamily = inter, fontFamily = inter,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
fontSize = 16.sp.scaled(), fontSize = 17.sp,
lineHeight = 21.sp.scaled(), lineHeight = 21.sp,
letterSpacing = 0.sp, letterSpacing = 0.sp,
), ),
) )
val iconSize = 15.dp
@@ -21,6 +21,7 @@ object Constants {
const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1024 const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1024
const val SUBSCRIPTION_TIMEOUT = 5_000L const val SUBSCRIPTION_TIMEOUT = 5_000L
const val FOCUS_REQUEST_DELAY = 500L
const val TRANSITION_ANIMATION_TIME = 200 const val TRANSITION_ANIMATION_TIME = 200
@@ -1,13 +1,19 @@
package com.zaneschepke.wireguardautotunnel.util package com.zaneschepke.wireguardautotunnel.util
import android.content.ContentValues
import android.content.Context import android.content.Context
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.provider.MediaStore.MediaColumns
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.BufferedOutputStream import timber.log.Timber
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.OutputStream
import java.time.Instant
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream import java.util.zip.ZipOutputStream
@@ -16,60 +22,73 @@ class FileUtils(
private val ioDispatcher: CoroutineDispatcher, private val ioDispatcher: CoroutineDispatcher,
) { ) {
suspend fun createWgFiles(tunnels: TunnelConfigs): List<File> { fun createWgFiles(tunnels: TunnelConfigs): List<File> {
return withContext(ioDispatcher) { return tunnels.map { config ->
tunnels.map { config -> val file = File(context.cacheDir, "${config.name}-wg.conf")
val file = File(context.cacheDir, "${config.name}-wg.conf") file.outputStream().use {
file.outputStream().use { it.write(config.wgQuick.toByteArray())
it.write(config.wgQuick.toByteArray())
}
file
} }
}
}
suspend fun createAmFiles(tunnels: TunnelConfigs): List<File> {
return withContext(ioDispatcher) {
tunnels.filter { it.amQuick != TunnelConfig.AM_QUICK_DEFAULT }.map { config ->
val file = File(context.cacheDir, "${config.name}-am.conf")
file.outputStream().use {
it.write(config.amQuick.toByteArray())
}
file
}
}
}
suspend fun zipAll(zipFile: File, files: List<File>) {
withContext(ioDispatcher) {
ZipOutputStream(BufferedOutputStream(FileOutputStream(zipFile))).use { zos ->
files.forEach { file ->
val zipFileName = (
file.parentFile?.let { parent ->
file.absolutePath.removePrefix(parent.absolutePath)
} ?: file.absolutePath
).removePrefix("/")
val entry = ZipEntry("$zipFileName${(if (file.isDirectory) "/" else "")}")
zos.putNextEntry(entry)
if (file.isFile) {
file.inputStream().use {
it.copyTo(zos)
}
}
}
}
}
}
suspend fun createNewShareFile(name: String): File {
return withContext(ioDispatcher) {
val sharePath = File(context.filesDir, "external_files")
if (sharePath.exists()) sharePath.delete()
sharePath.mkdir()
val file = File("${sharePath.path}/$name")
if (file.exists()) file.delete()
file.createNewFile()
file file
} }
} }
fun createAmFiles(tunnels: TunnelConfigs): List<File> {
return tunnels.filter { it.amQuick != TunnelConfig.AM_QUICK_DEFAULT }.map { config ->
val file = File(context.cacheDir, "${config.name}-am.conf")
file.outputStream().use {
it.write(config.amQuick.toByteArray())
}
file
}
}
suspend fun saveFilesToZip(files: List<File>): Result<Unit> {
return withContext(ioDispatcher) {
try {
val zipOutputStream =
createDownloadsFileOutputStream(
"wg-export_${Instant.now().epochSecond}.zip",
Constants.ZIP_FILE_MIME_TYPE,
)
ZipOutputStream(zipOutputStream).use { zos ->
files.forEach { file ->
val entry = ZipEntry(file.name)
zos.putNextEntry(entry)
if (file.isFile) {
file.inputStream().use { fis -> fis.copyTo(zos) }
}
}
return@withContext Result.success(Unit)
}
} catch (e: Exception) {
Timber.e(e)
Result.failure(ConfigExportException)
}
}
}
// TODO issue with android 9
private fun createDownloadsFileOutputStream(fileName: String, mimeType: String = Constants.ALL_FILE_TYPES): OutputStream? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val resolver = context.contentResolver
val contentValues =
ContentValues().apply {
put(MediaColumns.DISPLAY_NAME, fileName)
put(MediaColumns.MIME_TYPE, mimeType)
put(MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
}
val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
if (uri != null) {
return resolver.openOutputStream(uri)
}
} else {
val target =
File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
fileName,
)
return target.outputStream()
}
return null
}
} }
@@ -1,30 +0,0 @@
package com.zaneschepke.wireguardautotunnel.util
import android.content.res.Resources
import androidx.core.os.ConfigurationCompat
import com.zaneschepke.wireguardautotunnel.BuildConfig
object LocaleUtil {
private const val DEFAULT_LANG = "en"
val supportedLocales: Array<String> = BuildConfig.LANGUAGES
const val OPTION_PHONE_LANGUAGE = "sys_def"
/**
* returns the locale to use depending on the preference value
* when preference value = "sys_def" returns the locale of current system
* else it returns the locale code e.g. "en", "bn" etc.
*/
fun getLocaleFromPrefCode(prefCode: String): String {
val localeCode = if (prefCode != OPTION_PHONE_LANGUAGE) {
prefCode
} else {
val systemLang = ConfigurationCompat.getLocales(Resources.getSystem().configuration).get(0)?.language ?: DEFAULT_LANG
if (systemLang in supportedLocales) {
systemLang
} else {
DEFAULT_LANG
}
}
return localeCode
}
}
@@ -1,26 +0,0 @@
package com.zaneschepke.wireguardautotunnel.util
open class SingletonHolder<out T : Any, in A>(creator: (A) -> T) {
private var creator: ((A) -> T)? = creator
@Volatile private var instance: T? = null
fun getInstance(arg: A): T {
val i = instance
if (i != null) {
return i
}
return synchronized(this) {
val i2 = instance
if (i2 != null) {
i2
} else {
val created = creator!!(arg)
instance = created
creator = null
created
}
}
}
}
@@ -2,28 +2,18 @@ package com.zaneschepke.wireguardautotunnel.util.extensions
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Context.POWER_SERVICE
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.location.LocationManager
import android.net.Uri import android.net.Uri
import android.os.PowerManager
import android.provider.Settings import android.provider.Settings
import android.service.quicksettings.TileService import android.service.quicksettings.TileService
import android.widget.Toast import android.widget.Toast
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.core.location.LocationManagerCompat
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.receiver.BackgroundActionReceiver
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
private const val BASELINE_HEIGHT = 2201
private const val BASELINE_WIDTH = 1080
private const val BASELINE_DENSITY = 2.625
private const val ANDROID_TV_SIZE_MULTIPLIER = 1.5f
fun Context.openWebUrl(url: String): Result<Unit> { fun Context.openWebUrl(url: String): Result<Unit> {
return kotlin.runCatching { return kotlin.runCatching {
val webpage: Uri = Uri.parse(url) val webpage: Uri = Uri.parse(url)
@@ -36,52 +26,6 @@ fun Context.openWebUrl(url: String): Result<Unit> {
} }
} }
fun Context.isBatteryOptimizationsDisabled(): Boolean {
val pm = getSystemService(POWER_SERVICE) as PowerManager
return pm.isIgnoringBatteryOptimizations(packageName)
}
val Context.actionBarSize
get() = theme.obtainStyledAttributes(intArrayOf(android.R.attr.actionBarSize))
.let { attrs -> attrs.getDimension(0, 0F).toInt().also { attrs.recycle() } }
fun Context.resizeHeight(dp: Dp): Dp {
val displayMetrics = resources.displayMetrics
val density = displayMetrics.density
val height = (displayMetrics.heightPixels - this.actionBarSize) *
(if (isRunningOnTv()) ANDROID_TV_SIZE_MULTIPLIER else 1f)
val resizeHeightPercentage =
(height.toFloat() / BASELINE_HEIGHT) * (BASELINE_DENSITY.toFloat() / density)
return dp * resizeHeightPercentage
}
fun Context.resizeHeight(textUnit: TextUnit): TextUnit {
val displayMetrics = resources.displayMetrics
val density = displayMetrics.density
val height = (displayMetrics.heightPixels - actionBarSize) *
(if (isRunningOnTv()) ANDROID_TV_SIZE_MULTIPLIER else 1f)
val resizeHeightPercentage =
(height.toFloat() / BASELINE_HEIGHT) * (BASELINE_DENSITY.toFloat() / density)
return textUnit * resizeHeightPercentage * 1.1
}
fun Context.resizeWidth(dp: Dp): Dp {
val displayMetrics = resources.displayMetrics
val density = displayMetrics.density
val width = displayMetrics.widthPixels
val resizeWidthPercentage =
(width.toFloat() / BASELINE_WIDTH) * (BASELINE_DENSITY.toFloat() / density)
return dp * resizeWidthPercentage
}
fun Context.launchNotificationSettings() {
if (isRunningOnTv()) return launchAppSettings()
val settingsIntent: Intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
this.startActivity(settingsIntent)
}
fun Context.launchShareFile(file: Uri) { fun Context.launchShareFile(file: Uri) {
val shareIntent = Intent().apply { val shareIntent = Intent().apply {
setAction(Intent.ACTION_SEND) setAction(Intent.ACTION_SEND)
@@ -92,14 +36,6 @@ fun Context.launchShareFile(file: Uri) {
this.startActivity(Intent.createChooser(shareIntent, "")) this.startActivity(Intent.createChooser(shareIntent, ""))
} }
fun Context.isLocationServicesEnabled(): Boolean {
val locationManager =
getSystemService(
Context.LOCATION_SERVICE,
) as LocationManager
return LocationManagerCompat.isLocationEnabled(locationManager)
}
fun Context.showToast(resId: Int) { fun Context.showToast(resId: Int) {
Toast.makeText( Toast.makeText(
this, this,
@@ -168,23 +104,23 @@ fun Context.launchAppSettings() {
} }
} }
// fun Context.startTunnelBackground(tunnelId: Int) { fun Context.startTunnelBackground(tunnelId: Int) {
// sendBroadcast( sendBroadcast(
// Intent(this, BackgroundActionReceiver::class.java).apply { Intent(this, BackgroundActionReceiver::class.java).apply {
// action = BackgroundActionReceiver.ACTION_CONNECT action = BackgroundActionReceiver.ACTION_CONNECT
// putExtra(BackgroundActionReceiver.TUNNEL_ID_EXTRA_KEY, tunnelId) putExtra(BackgroundActionReceiver.TUNNEL_ID_EXTRA_KEY, tunnelId)
// }, },
// ) )
// } }
//
// fun Context.stopTunnelBackground(tunnelId: Int) { fun Context.stopTunnelBackground(tunnelId: Int) {
// sendBroadcast( sendBroadcast(
// Intent(this, BackgroundActionReceiver::class.java).apply { Intent(this, BackgroundActionReceiver::class.java).apply {
// action = BackgroundActionReceiver.ACTION_DISCONNECT action = BackgroundActionReceiver.ACTION_DISCONNECT
// putExtra(BackgroundActionReceiver.TUNNEL_ID_EXTRA_KEY, tunnelId) putExtra(BackgroundActionReceiver.TUNNEL_ID_EXTRA_KEY, tunnelId)
// }, },
// ) )
// } }
fun Context.requestTunnelTileServiceStateUpdate() { fun Context.requestTunnelTileServiceStateUpdate() {
TileService.requestListeningState( TileService.requestListeningState(
@@ -28,9 +28,9 @@ fun String.extractNameAndNumber(): Pair<String, Int>? {
} }
fun List<String>.isMatchingToWildcardList(value: String): Boolean { fun List<String>.isMatchingToWildcardList(value: String): Boolean {
val excludeValues = this.filter { it.startsWith("!") }.map { it.removePrefix("!").transformWildcardsToRegex() } val excludeValues = this.filter { it.startsWith("!") }.map { it.removePrefix("!").toRegexWithWildcards() }
Timber.d("Excluded values: $excludeValues") Timber.d("Excluded values: $excludeValues")
val includedValues = this.filter { !it.startsWith("!") }.map { it.transformWildcardsToRegex() } val includedValues = this.filter { !it.startsWith("!") }.map { it.toRegexWithWildcards() }
Timber.d("Included values: $includedValues") Timber.d("Included values: $includedValues")
val matches = includedValues.filter { it.matches(value) } val matches = includedValues.filter { it.matches(value) }
val excludedMatches = excludeValues.filter { it.matches(value) } val excludedMatches = excludeValues.filter { it.matches(value) }
@@ -39,32 +39,6 @@ fun List<String>.isMatchingToWildcardList(value: String): Boolean {
return matches.isNotEmpty() && excludedMatches.isEmpty() return matches.isNotEmpty() && excludedMatches.isEmpty()
} }
fun String.transformWildcardsToRegex(): Regex { fun String.toRegexWithWildcards(): Regex {
return this.replaceUnescapedChar("*", ".*").replaceUnescapedChar("?", ".").toRegex() return this.replace("*", ".*").replace("?", ".").toRegex()
}
fun String.replaceUnescapedChar(charToReplace: String, replacement: String): String {
val escapedChar = Regex.escape(charToReplace)
val regex = "(?<!\\\\)(?<!(?<!\\\\)\\\\)($escapedChar)".toRegex()
return regex.replace(this) { matchResult ->
if (matchResult.range.first == 0 ||
this[matchResult.range.first - 1] != '\\' ||
(matchResult.range.first > 1 && this[matchResult.range.first - 2] == '\\')
) {
replacement.toString()
} else {
matchResult.value
}
}
}
fun String.isCharacterEscaped(index: Int): Boolean {
if (index <= 0) return false
var backslashCount = 0
var currentIndex = index - 1
while (currentIndex >= 0 && this[currentIndex] == '\\') {
backslashCount++
currentIndex--
}
return backslashCount % 2 != 0
} }
@@ -5,8 +5,8 @@ import com.wireguard.android.util.RootShell
import com.wireguard.config.Peer import com.wireguard.config.Peer
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.ui.theme.Corn
import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree
import com.zaneschepke.wireguardautotunnel.ui.theme.Straw
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.NumberUtils import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import org.amnezia.awg.config.Config import org.amnezia.awg.config.Config
@@ -58,7 +58,7 @@ fun TunnelStatistics?.asColor(): Color {
?.let { statuses -> ?.let { statuses ->
when { when {
statuses.all { it == HandshakeStatus.HEALTHY } -> SilverTree statuses.all { it == HandshakeStatus.HEALTHY } -> SilverTree
statuses.any { it == HandshakeStatus.STALE } -> Straw statuses.any { it == HandshakeStatus.STALE } -> Corn
statuses.all { it == HandshakeStatus.NOT_STARTED } -> Color.Gray statuses.all { it == HandshakeStatus.NOT_STARTED } -> Color.Gray
else -> Color.Gray else -> Color.Gray
} }
@@ -1,43 +1,10 @@
package com.zaneschepke.wireguardautotunnel.util.extensions package com.zaneschepke.wireguardautotunnel.util.extensions
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavGraph.Companion.findStartDestination
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.ui.Route import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.isCurrentRoute
fun NavController.navigateAndForget(route: Route) { fun NavController.navigateAndForget(route: Route) {
navigate(route) { navigate(route) {
popUpTo(0) popUpTo(0)
} }
} }
fun NavController.goFromRoot(route: Route) {
if (currentBackStackEntry?.isCurrentRoute(route::class) == true) return
this.navigate(route) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(graph.findStartDestination().id) {
saveState = true
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
restoreState = true
}
}
fun Dp.scaledHeight(): Dp {
return WireGuardAutoTunnel.instance.resizeHeight(this)
}
fun Dp.scaledWidth(): Dp {
return WireGuardAutoTunnel.instance.resizeWidth(this)
}
fun TextUnit.scaled(): TextUnit {
return WireGuardAutoTunnel.instance.resizeHeight(this)
}
+9
View File
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#e8eaed"
android:pathData="M440,520L200,520v-80h240v-240h80v240h240v80L520,520v240h-80v-240Z" />
</vector>
+9
View File
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#e8eaed"
android:pathData="m256,760 l-56,-56 224,-224 -224,-224 56,-56 224,224 224,-224 56,56 -224,224 224,224 -56,56 -224,-224 -224,224Z" />
</vector>
+10
View File
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="800dp"
android:height="800dp"
android:viewportWidth="256"
android:viewportHeight="256">
<path
android:fillColor="#5865F2"
android:fillType="nonZero"
android:pathData="M216.86,45.1C200.29,37.34 182.57,31.71 164.04,28.5C161.77,32.61 159.11,38.15 157.28,42.55C137.58,39.58 118.07,39.58 98.74,42.55C96.91,38.15 94.19,32.61 91.9,28.5C73.35,31.71 55.61,37.36 39.04,45.14C5.62,95.65 -3.44,144.9 1.09,193.46C23.26,210.01 44.74,220.07 65.86,226.65C71.08,219.47 75.73,211.84 79.74,203.8C72.1,200.9 64.79,197.32 57.89,193.17C59.72,191.81 61.51,190.39 63.24,188.93C105.37,208.63 151.13,208.63 192.75,188.93C194.51,190.39 196.3,191.81 198.11,193.17C191.18,197.34 183.85,200.92 176.22,203.82C180.23,211.84 184.86,219.49 190.1,226.67C211.24,220.09 232.74,210.03 254.91,193.46C260.23,137.17 245.83,88.37 216.86,45.1ZM85.47,163.59C72.83,163.59 62.46,151.79 62.46,137.41C62.46,123.04 72.61,111.21 85.47,111.21C98.34,111.21 108.71,123.02 108.49,137.41C108.51,151.79 98.34,163.59 85.47,163.59ZM170.53,163.59C157.88,163.59 147.51,151.79 147.51,137.41C147.51,123.04 157.66,111.21 170.53,111.21C183.39,111.21 193.76,123.02 193.54,137.41C193.54,151.79 183.39,163.59 170.53,163.59Z" />
</vector>
+9
View File
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#e8eaed"
android:pathData="M200,760h57l391,-391 -57,-57 -391,391v57ZM120,840v-170l528,-527q12,-11 26.5,-17t30.5,-6q16,0 31,6t26,18l55,56q12,11 17.5,26t5.5,30q0,16 -5.5,30.5T817,313L290,840L120,840ZM760,256 L704,200 760,256ZM619,341 L591,312 648,369 619,341Z" />
</vector>
+45 -1
View File
@@ -1,8 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="no_tunnels">Žádné tunely ještě nebyly přidány!</string> <string name="no_tunnels">Žádné tunely ještě nebyly přidány!</string>
<string name="watcher_notification_text_paused">Monitorování změn ve stavu sítě: pozastaveno</string>
<string name="notification_permission_required">Je vyžadováno oprávnění pro oznámení.</string>
<string name="add_trusted_ssid">Přidat název důvěryhodné Wi-Fi</string>
<string name="enable_auto_tunnel">Spustit automatické tunelování</string>
<string name="tunnel_mobile_data">Tunelování na mobilních datech</string> <string name="tunnel_mobile_data">Tunelování na mobilních datech</string>
<string name="privacy_policy">Otevřít zásady soukromí</string> <string name="privacy_policy">Otevřít zásady soukromí</string>
<string name="okay">OK</string> <string name="okay">OK</string>
<string name="thank_you">Děkujeme za používání WG Tunnel!</string> <string name="thank_you">Děkujeme za používání WG Tunnel!</string>
<string name="add_tunnels_text">Přidat ze souboru nebo zipu</string> <string name="add_tunnels_text">Přidat ze souboru nebo zipu</string>
@@ -10,16 +14,23 @@
<string name="add_from_qr">Přidat z QR kódu</string> <string name="add_from_qr">Přidat z QR kódu</string>
<string name="qr_scan">QR skenování</string> <string name="qr_scan">QR skenování</string>
<string name="tunnel_name">Název tunelu</string> <string name="tunnel_name">Název tunelu</string>
<string name="add_tunnel">Přidat tunel</string>
<string name="exclude">Vyloučit</string> <string name="exclude">Vyloučit</string>
<string name="include">Zahrnout</string> <string name="include">Zahrnout</string>
<string name="save_changes">Uložit</string> <string name="save_changes">Uložit</string>
<string name="endpoint">Koncový bod</string> <string name="endpoint">Koncový bod</string>
<string name="vpn_connection_failed">Připojení selhalo</string>
<string name="always_on_vpn_support">Povolit trvalé připojení VPN</string> <string name="always_on_vpn_support">Povolit trvalé připojení VPN</string>
<string name="location_services_not_detected">Služby polohy nebyly detekovány</string> <string name="location_services_not_detected">Služby polohy nebyly detekovány</string>
<string name="hint_search_packages">Hledat balíčky</string> <string name="hint_search_packages">Hledat balíčky</string>
<string name="attempt_connection">Pokus o připojení...</string>
<string name="vpn_starting">VPN se spouští</string>
<string name="vpn_on">VPN zapnuto</string> <string name="vpn_on">VPN zapnuto</string>
<string name="vpn_off">VPN vypnuto</string> <string name="vpn_off">VPN vypnuto</string>
<string name="default_vpn_on">Hlavní VPN zapnuto</string>
<string name="default_vpn_off">Hlavní VPN vypnuto</string>
<string name="create_import">Vytvořit od základu</string> <string name="create_import">Vytvořit od základu</string>
<string name="turn_off_auto">Akce vyžaduje, aby bylo automatické tunelování vypnuté či pozastavené</string>
<string name="add_peer">Přidat peer</string> <string name="add_peer">Přidat peer</string>
<string name="comma_separated_list">seznam oddělený čárkami</string> <string name="comma_separated_list">seznam oddělený čárkami</string>
<string name="random">(náhodné)</string> <string name="random">(náhodné)</string>
@@ -32,7 +43,12 @@
<string name="error_authorization_failed">Nepovedlo se autorizovat</string> <string name="error_authorization_failed">Nepovedlo se autorizovat</string>
<string name="enabled_app_shortcuts">Zapnout zkratky</string> <string name="enabled_app_shortcuts">Zapnout zkratky</string>
<string name="export_configs">Exportovat konfigurace</string> <string name="export_configs">Exportovat konfigurace</string>
<string name="export_configs_failed">Nepovedlo se exportovat konfigurace</string>
<string name="location_services_required">Služby polohy vyžadovány</string>
<string name="background_location_required">Oprávnění pro polohu na pozadí vyžadováno</string>
<string name="precise_location_required">Oprávnění pro přesnou polohu vyžadováno</string>
<string name="unknown_error">Došlo k neznámé chybě</string> <string name="unknown_error">Došlo k neznámé chybě</string>
<string name="exported_configs_message">Konfigurace exportovány do stažených souborů</string>
<string name="tunnel_on_wifi">Tunelovat na nedůvěryhodné Wi-Fi</string> <string name="tunnel_on_wifi">Tunelovat na nedůvěryhodné Wi-Fi</string>
<string name="email_subject">WG Tunnel podpora</string> <string name="email_subject">WG Tunnel podpora</string>
<string name="email_chooser">Poslat email…</string> <string name="email_chooser">Poslat email…</string>
@@ -41,14 +57,22 @@
<string name="error_root_denied">Oprávnění root zamítnuto</string> <string name="error_root_denied">Oprávnění root zamítnuto</string>
<string name="error_no_file_explorer">Žádný průzkumník souborů není nainstalován</string> <string name="error_no_file_explorer">Žádný průzkumník souborů není nainstalován</string>
<string name="error_invalid_code">Neplatný QR kód</string> <string name="error_invalid_code">Neplatný QR kód</string>
<string name="error_none">Žádná chyba</string>
<string name="auto_tunnel_title">Služba automatického tunelování</string> <string name="auto_tunnel_title">Služba automatického tunelování</string>
<string name="resume">Obnovit</string>
<string name="active">aktivní</string>
<string name="open_issue">Otevřít případ</string> <string name="open_issue">Otevřít případ</string>
<string name="incorrect_pin">PIN je nesprávný</string> <string name="incorrect_pin">PIN je nesprávný</string>
<string name="create_pin">Vytvořte PIN</string> <string name="create_pin">Vytvořte PIN</string>
<string name="set_primary_tunnel">Nastavit jako hlavní tunel</string> <string name="set_primary_tunnel">Nastavit jako hlavní tunel</string>
<string name="use_tunnel_on_wifi_name">Použít tunel pro Wi-Fi</string> <string name="use_tunnel_on_wifi_name">Použít tunel pro Wi-Fi</string>
<string name="edit_tunnel">Upravit tunel</string> <string name="edit_tunnel">Upravit tunel</string>
<string name="disabled">vypnuto</string>
<string name="auto_tun_on">Obnovit automatické tunelování</string>
<string name="auto_tun_off">Pozastavit automatické tunelování</string>
<string name="version">Verze</string> <string name="version">Verze</string>
<string name="mode">Mód</string>
<string name="use_amnezia">"Použít Amnezia userspace "</string>
<string name="junk_packet_count">Junk packet počet</string> <string name="junk_packet_count">Junk packet počet</string>
<string name="junk_packet_minimum_size">Junk packet minimální velikost</string> <string name="junk_packet_minimum_size">Junk packet minimální velikost</string>
<string name="junk_packet_maximum_size">Junk packet maximální velikost</string> <string name="junk_packet_maximum_size">Junk packet maximální velikost</string>
@@ -60,17 +84,28 @@
<string name="error_file_format">Neplatný formát konfigurace</string> <string name="error_file_format">Neplatný formát konfigurace</string>
<string name="error_file_extension">Soubor není ve formátu .conf nebo .zip</string> <string name="error_file_extension">Soubor není ve formátu .conf nebo .zip</string>
<string name="turn_off_tunnel">Akce vyžaduje vypnutí tunelu</string> <string name="turn_off_tunnel">Akce vyžaduje vypnutí tunelu</string>
<string name="watcher_notification_text_active">Monitorování změn ve stavu sítě: aktivní</string>
<string name="tunnel_on_ethernet">Tunelovat na ethernetu</string> <string name="tunnel_on_ethernet">Tunelovat na ethernetu</string>
<string name="tunnel_start_title">VPN připojeno</string>
<string name="prominent_background_location_message">Tato funkce vyžaduje oprávnění pro přístup k poloze na pozadí pro zapnutí monitorování Wi-Fi SSID, i když je aplikace zavřená. Pro více detailů, podívejte se prosím na zásady soukromí umístěné v kategorii Podpora.</string> <string name="prominent_background_location_message">Tato funkce vyžaduje oprávnění pro přístup k poloze na pozadí pro zapnutí monitorování Wi-Fi SSID, i když je aplikace zavřená. Pro více detailů, podívejte se prosím na zásady soukromí umístěné v kategorii Podpora.</string>
<string name="tunnel_start_text">Připojeno k tunelu</string>
<string name="tunnels">Tunely</string> <string name="tunnels">Tunely</string>
<string name="disable_auto_tunnel">Zastavit automatické tunelování</string>
<string name="tunnel_all">Tunelovat všechny aplikace</string> <string name="tunnel_all">Tunelovat všechny aplikace</string>
<string name="config_changes_saved">Změny v konfiguraci uloženy.</string> <string name="config_changes_saved">Změny v konfiguraci uloženy.</string>
<string name="icon">Ikona</string> <string name="icon">Ikona</string>
<string name="no_thanks">Ne, děkuji</string>
<string name="turn_on">Zapnout</string>
<string name="map">Mapa</string>
<string name="public_key">Veřejný klíč</string> <string name="public_key">Veřejný klíč</string>
<string name="addresses">Adresy</string> <string name="addresses">Adresy</string>
<string name="dns_servers">DNS servery</string> <string name="dns_servers">DNS servery</string>
<string name="allowed_ips">Povolené IP adresy</string> <string name="allowed_ips">Povolené IP adresy</string>
<string name="name">Název</string> <string name="name">Název</string>
<string name="restart">Restartovat tunel</string>
<string name="scanning_qr">Skenování QR</string>
<string name="none">Žádné názvy důvěryhodných Wi-Fi</string>
<string name="other">Ostatní</string>
<string name="auto_tunneling">Automatické tunelování</string> <string name="auto_tunneling">Automatické tunelování</string>
<string name="turn_on_tunnel">Akce vyžaduje aktivní tunel</string> <string name="turn_on_tunnel">Akce vyžaduje aktivní tunel</string>
<string name="interface_">Rozhraní</string> <string name="interface_">Rozhraní</string>
@@ -81,22 +116,30 @@
<string name="base64_key">base64 klíč</string> <string name="base64_key">base64 klíč</string>
<string name="docs_description">Přečíst si dokumentaci</string> <string name="docs_description">Přečíst si dokumentaci</string>
<string name="email_description">Poslat mi email</string> <string name="email_description">Poslat mi email</string>
<string name="support_help_text">Pokud máte potíže, nápady pro zlepšení, nebo se chcete jen zapojit, následující prostředky jsou k dispozici:</string>
<string name="location_services_missing_message">Aplikace nenašla žádné služby polohy zapnuté na Vašem zařízení. Dle Vašeho zařízení, tohle může způsobit, že funkce nedůvěryhodné Wi-Fi nedokáže přečíst jméno připojené Wi-Fi. Chcete i přesto pokračovat?</string> <string name="location_services_missing_message">Aplikace nenašla žádné služby polohy zapnuté na Vašem zařízení. Dle Vašeho zařízení, tohle může způsobit, že funkce nedůvěryhodné Wi-Fi nedokáže přečíst jméno připojené Wi-Fi. Chcete i přesto pokračovat?</string>
<string name="delete_tunnel">Smazat tunel</string> <string name="delete_tunnel">Smazat tunel</string>
<string name="delete_tunnel_message">Jste si jisti, že chcete smazat tento tunel?</string> <string name="delete_tunnel_message">Jste si jisti, že chcete smazat tento tunel?</string>
<string name="yes">Ano</string> <string name="yes">Ano</string>
<string name="pause">Pozastavit</string>
<string name="paused">pozastaveno</string>
<string name="tunneling_apps">Tunelování aplikací</string> <string name="tunneling_apps">Tunelování aplikací</string>
<string name="all">vše</string> <string name="all">vše</string>
<string name="included">zahrnuto</string> <string name="included">zahrnuto</string>
<string name="excluded">vyloučeno</string> <string name="excluded">vyloučeno</string>
<string name="always_on_disabled">Trvalé VPN připojení se pokusilo spustit tunel, ale tato funkce je vypnutá v nastavení.</string>
<string name="no_email_detected">Žádná emailová aplikace nebyla nalezena</string> <string name="no_email_detected">Žádná emailová aplikace nebyla nalezena</string>
<string name="no_browser_detected">Žádný prohlížeč nebyl nalezen</string> <string name="no_browser_detected">Žádný prohlížeč nebyl nalezen</string>
<string name="logs_saved">Logy uloženy do stažených souborů</string>
<string name="read_logs">Přečíst si logy</string> <string name="read_logs">Přečíst si logy</string>
<string name="config_parse_error">Nepovedlo se vložit konfiguraci</string>
<string name="pin_created">PIN úspěšně vytvořen</string> <string name="pin_created">PIN úspěšně vytvořen</string>
<string name="enter_pin">Vložte Váš PIN</string> <string name="enter_pin">Vložte Váš PIN</string>
<string name="no_wifi_names_configured">Žádné názvy Wi-Fi nebyly nastaveny pro tento tunel</string>
<string name="enable_app_lock">Zapnout zámek aplikace</string> <string name="enable_app_lock">Zapnout zámek aplikace</string>
<string name="restart_on_ping">Restartovat při selhání pingu</string> <string name="restart_on_ping">Restartovat při selhání pingu</string>
<string name="mobile_data_tunnel">Nastavit jako tunel pro mobilní data</string> <string name="mobile_data_tunnel">Nastavit jako tunel pro mobilní data</string>
<string name="general">Obecné</string>
<string name="settings">Nastavení</string> <string name="settings">Nastavení</string>
<string name="support">Podpora</string> <string name="support">Podpora</string>
<string name="app_name">WG Tunnel</string> <string name="app_name">WG Tunnel</string>
@@ -105,4 +148,5 @@
<string name="listen_port">Naslouchací port</string> <string name="listen_port">Naslouchací port</string>
<string name="auto">(automaticky)</string> <string name="auto">(automaticky)</string>
<string name="kernel">Kernel</string> <string name="kernel">Kernel</string>
<string name="backend">Backend</string>
</resources> </resources>
+58 -30
View File
@@ -3,28 +3,43 @@
<string name="app_name">WG Tunnel</string> <string name="app_name">WG Tunnel</string>
<string name="error_file_extension">Datei ist keine .conf oder .zip</string> <string name="error_file_extension">Datei ist keine .conf oder .zip</string>
<string name="no_tunnels">Noch keine Tunnel hinzugefügt!</string> <string name="no_tunnels">Noch keine Tunnel hinzugefügt!</string>
<string name="watcher_notification_text_active">Überwachung der Netzwerkänderungen: aktiv</string>
<string name="tunnels">Tunnel</string> <string name="tunnels">Tunnel</string>
<string name="enable_auto_tunnel">Auto-Tunneln starten</string>
<string name="tunnel_mobile_data">Tunnel für mobile Daten</string> <string name="tunnel_mobile_data">Tunnel für mobile Daten</string>
<string name="privacy_policy">Datenschutzbestimmungen anzeigen</string> <string name="privacy_policy">Datenschutzbestimmungen anzeigen</string>
<string name="disable_auto_tunnel">Auto-Tunneln stoppen</string>
<string name="okay">Ok</string> <string name="okay">Ok</string>
<string name="tunnel_on_ethernet">Tunnel für Ethernet</string> <string name="tunnel_on_ethernet">Tunnel für Ethernet</string>
<string name="attempt_connection">Verbindungsversuch..</string>
<string name="auto_tunneling">Auto-Tunneln</string> <string name="auto_tunneling">Auto-Tunneln</string>
<string name="default_vpn_off">Primärer VPN aus</string>
<string name="turn_on_tunnel">Für diese Aktion muss ein aktiver Tunnel bestehen</string> <string name="turn_on_tunnel">Für diese Aktion muss ein aktiver Tunnel bestehen</string>
<string name="watcher_notification_text_paused">Überwachung der Netzwerkänderungen: pausiert</string>
<string name="tunnel_start_title">VPN verbunden</string>
<string name="tunnel_start_text">Mit Tunnel verbunden</string>
<string name="notification_permission_required">Benachrichtigungsberechtigung benötigt.</string>
<string name="add_trusted_ssid">Vertrauenswürdigen WLAN-Namen hinzufügen</string>
<string name="prominent_background_location_message">Diese Funktion erfordert die Erlaubnis zur Standortbestimmung im Hintergrund, um die Überwachung der WLAN SSID zu ermöglichen, auch wenn die Anwendung geschlossen ist. Weitere Einzelheiten in den Datenschutzbestimmungen, die auf dem Support-Bildschirm verlinkt sind.</string> <string name="prominent_background_location_message">Diese Funktion erfordert die Erlaubnis zur Standortbestimmung im Hintergrund, um die Überwachung der WLAN SSID zu ermöglichen, auch wenn die Anwendung geschlossen ist. Weitere Einzelheiten in den Datenschutzbestimmungen, die auf dem Support-Bildschirm verlinkt sind.</string>
<string name="prominent_background_location_title">Vereinbarung der Standortberechtigung im Hintergrund</string> <string name="prominent_background_location_title">Vereinbarung der Standortberechtigung im Hintergrund</string>
<string name="thank_you">Danke fürs Benutzen von WG Tunnel!</string> <string name="thank_you">Danke fürs Benutzen von WG Tunnel!</string>
<string name="trusted_ssid_value_description">SSID übermitteln</string> <string name="trusted_ssid_empty_description">SSID eingeben</string>
<string name="trusted_ssid_value_description">SSID bestätigen</string>
<string name="add_tunnels_text">Von Datei oder ZIP hinzufügen</string> <string name="add_tunnels_text">Von Datei oder ZIP hinzufügen</string>
<string name="open_file">Datei öffnen</string> <string name="open_file">Datei öffnen</string>
<string name="add_from_qr">Über QR-Code hinzufügen</string> <string name="add_from_qr">Über QR-Code hinzufügen</string>
<string name="qr_scan">QR--Code scannen</string> <string name="qr_scan">Scanne QR</string>
<string name="tunnel_name">Tunnel-Name</string> <string name="tunnel_name">Tunnel Name</string>
<string name="add_tunnel">Tunnel hinzufügen</string>
<string name="exclude">Ausschließen</string> <string name="exclude">Ausschließen</string>
<string name="include">Einschließen</string> <string name="include">Einschließen</string>
<string name="tunnel_all">Alle Apps tunneln</string> <string name="tunnel_all">Alle Apps tunneln</string>
<string name="config_changes_saved">Konfigurationsänderungen gespeichert.</string> <string name="config_changes_saved">Konfigurationsänderungen gespeichert.</string>
<string name="save_changes">Speichern</string> <string name="save_changes">Speichern</string>
<string name="icon">Symbol</string> <string name="icon">Symbol</string>
<string name="no_thanks">Nein danke</string>
<string name="turn_on">Einschalten</string>
<string name="map">Karte</string>
<string name="public_key">Öffentlicher Schlüssel</string> <string name="public_key">Öffentlicher Schlüssel</string>
<string name="addresses">Adressen</string> <string name="addresses">Adressen</string>
<string name="dns_servers">DNS-Server</string> <string name="dns_servers">DNS-Server</string>
@@ -33,13 +48,21 @@
<string name="allowed_ips">Erlaubte IPs</string> <string name="allowed_ips">Erlaubte IPs</string>
<string name="endpoint">Endpunkt</string> <string name="endpoint">Endpunkt</string>
<string name="name">Name</string> <string name="name">Name</string>
<string name="restart">Tunnel neustarten</string>
<string name="vpn_connection_failed">Verbindung fehlgeschlagen</string>
<string name="always_on_vpn_support">Always-On VPN erlauben</string> <string name="always_on_vpn_support">Always-On VPN erlauben</string>
<string name="location_services_not_detected">Standortdienste nicht erkannt</string> <string name="location_services_not_detected">Standortdienste nicht erkannt</string>
<string name="hint_search_packages">Pakete suchen</string> <string name="hint_search_packages">Pakete suchen</string>
<string name="vpn_starting">VPN startet</string>
<string name="db_name">wg-tunnel-db</string> <string name="db_name">wg-tunnel-db</string>
<string name="scanning_qr">Scanne nach QR</string>
<string name="none">Keine vertrauenswürdigen WLAN Namen</string>
<string name="other">Sonstige</string>
<string name="vpn_on">VPN an</string> <string name="vpn_on">VPN an</string>
<string name="vpn_off">VPN aus</string> <string name="vpn_off">VPN aus</string>
<string name="default_vpn_on">Primärer VPN an</string>
<string name="create_import">Von Grund auf neu erstellen</string> <string name="create_import">Von Grund auf neu erstellen</string>
<string name="turn_off_auto">Für diese Aktion muss Auto-Tunneln ausgeschaltet oder pausiert sein</string>
<string name="add_peer">Peer hinzufügen</string> <string name="add_peer">Peer hinzufügen</string>
<string name="done">Erledigt</string> <string name="done">Erledigt</string>
<string name="rotate_keys">Schlüssel rotieren</string> <string name="rotate_keys">Schlüssel rotieren</string>
@@ -49,6 +72,7 @@
<string name="comma_separated_list">Kommaseparierte Liste</string> <string name="comma_separated_list">Kommaseparierte Liste</string>
<string name="delete_tunnel">Tunnel löschen</string> <string name="delete_tunnel">Tunnel löschen</string>
<string name="persistent_keepalive">Dauerhaftes Keepalive</string> <string name="persistent_keepalive">Dauerhaftes Keepalive</string>
<string name="background_location_required">Hintergrund Standortdienste erforderlich</string>
<string name="enable_app_lock">App-Sperre aktivieren</string> <string name="enable_app_lock">App-Sperre aktivieren</string>
<string name="interface_">Schnittstelle</string> <string name="interface_">Schnittstelle</string>
<string name="listen_port">Eingehender Port</string> <string name="listen_port">Eingehender Port</string>
@@ -59,44 +83,66 @@
<string name="cancel">Abbrechen</string> <string name="cancel">Abbrechen</string>
<string name="preshared_key">Geteilter Schlüssel</string> <string name="preshared_key">Geteilter Schlüssel</string>
<string name="enabled_app_shortcuts">App-Verknüpfungen aktivieren</string> <string name="enabled_app_shortcuts">App-Verknüpfungen aktivieren</string>
<string name="exported_configs_message">Konfigurationen in Download Ordner exportiert</string>
<string name="tunnel_on_wifi">Tunnel bei nicht vertrauenswürdigem WLAN</string> <string name="tunnel_on_wifi">Tunnel bei nicht vertrauenswürdigem WLAN</string>
<string name="email_subject">WG Tunnel Unterstützung</string> <string name="email_subject">WG Tunnel Unterstützung</string>
<string name="docs_description">Dokumentation lesen</string> <string name="docs_description">Dokumentation lesen</string>
<string name="email_description">Sende mir eine E-Mail</string> <string name="email_description">Sende mir eine E-Mail</string>
<string name="support_help_text">Bei Fehlern oder Verbesserungsvorschlägen stehen folgende Ressourcen zur Verfügung:</string>
<string name="error_root_denied">Root Shell verweigert</string> <string name="error_root_denied">Root Shell verweigert</string>
<string name="error_no_file_explorer">Kein Datei-Explorer installiert</string> <string name="error_no_file_explorer">Kein Datei-Explorer installiert</string>
<string name="location_services_missing_message">Die App erkennt keine auf deinem Gerät aktivierten Standortdienste. Je nach Gerät kann dies dazu führen, dass die Funktion \"Nicht vertrauenswürdiges WLAN\" den WLAN-Namen nicht lesen kann. Möchtest du trotzdem fortfahren?</string> <string name="location_services_missing_message">Die App erkennt keine auf deinem Gerät aktivierten Standortdienste. Je nach Gerät kann dies dazu führen, dass die Funktion \"Nicht vertrauenswürdiges WLAN\" den WLAN-Namen nicht lesen kann. Möchtest du trotzdem fortfahren?</string>
<string name="auto_tunnel_title">Auto-Tunnel Service</string> <string name="auto_tunnel_title">Auto-Tunnel Service</string>
<string name="delete_tunnel_message">Bist du sicher, dass du den Tunnel löschen möchtest?</string> <string name="delete_tunnel_message">Bist du sicher, dass du den Tunnel löschen möchtest?</string>
<string name="yes">Ja</string> <string name="yes">Ja</string>
<string name="resume">Fortsetzen</string>
<string name="pause">Pausieren</string>
<string name="paused">Pausiert</string>
<string name="active">Aktiv</string>
<string name="go">Los</string>
<string name="excluded">ausgeschlossen</string> <string name="excluded">ausgeschlossen</string>
<string name="all">alle</string> <string name="all">Alle</string>
<string name="always_on_disabled">Always-on VPN wollte einen Tunnel starten, aber dieses Feature ist in den Einstellungen deaktiviert.</string>
<string name="no_browser_detected">Keinen Browser erkannt</string> <string name="no_browser_detected">Keinen Browser erkannt</string>
<string name="open_issue">Issue öffnen</string> <string name="open_issue">Issue öffnen</string>
<string name="read_logs">Logs lesen</string> <string name="read_logs">Logs lesen</string>
<string name="auto">(automatisch)</string> <string name="auto">(automatisch)</string>
<string name="config_parse_error">Fehler beim Lesen der Konfiguration</string>
<string name="incorrect_pin">PIN nicht korrekt</string> <string name="incorrect_pin">PIN nicht korrekt</string>
<string name="pin_created">PIN erfolgreich erstellt</string> <string name="pin_created">PIN erfolgreich erstellt</string>
<string name="enter_pin">Deine PIN eingeben</string> <string name="enter_pin">Deine PIN eingeben</string>
<string name="auto_off">Auto-Tunneln pausieren</string>
<string name="auto_tun_on">Auto-Tunneln fortsetzen</string>
<string name="auto_tun_off">Auto-Tunneln pausieren</string>
<string name="version">Version</string> <string name="version">Version</string>
<string name="mode">Modus</string>
<string name="userspace">Benutzerfläche</string>
<string name="settings">Einstellungen</string> <string name="settings">Einstellungen</string>
<string name="support">Unterstützung</string> <string name="support">Unterstützung</string>
<string name="watcher_channel_id">Wächterkanal</string> <string name="watcher_channel_id">Wächterkanal</string>
<string name="error_authentication_failed">Authentifizierung fehlgeschlagen</string> <string name="error_authentication_failed">Anmeldung fehlgeschlagen</string>
<string name="export_configs">Konfigurationen exportieren</string> <string name="export_configs">Konfigurationen exportieren</string>
<string name="unknown_error">Unbekannter Fehler aufgetreten</string> <string name="unknown_error">Unbekannter Fehler aufgetreten</string>
<string name="email_chooser">Sende eine E-Mail…</string> <string name="email_chooser">Sende eine E-Mail…</string>
<string name="error_authorization_failed">Autorisierung fehlgeschlagen</string> <string name="error_authorization_failed">Autorisierung fehlgeschlagen</string>
<string name="location_services_required">Standortdienste erforderlich</string>
<string name="precise_location_required">Genauer Standort erforderlich</string>
<string name="error_invalid_code">Ungültiger QR Code</string> <string name="error_invalid_code">Ungültiger QR Code</string>
<string name="error_none">Kein Fehler</string>
<string name="tunneling_apps">Getunnelte Apps</string> <string name="tunneling_apps">Getunnelte Apps</string>
<string name="included">eingeschlossen</string> <string name="included">eingeschlossen</string>
<string name="no_email_detected">Keine E-Mail-App erkannt</string> <string name="no_email_detected">Keine E-Mail-App erkannt</string>
<string name="logs_saved">Logs im Download Ordner gespeichert</string>
<string name="create_pin">PIN erstellen</string> <string name="create_pin">PIN erstellen</string>
<string name="use_tunnel_on_wifi_name">Tunnel für WLAN-Namen verwenden</string> <string name="use_tunnel_on_wifi_name">Tunnel für WLAN-Namen verwenden</string>
<string name="no_wifi_names_configured">Keine WLAN-Namen für diesen Tunnel konfiguriert</string>
<string name="disabled">Deaktiviert</string>
<string name="mobile_data_tunnel">Als Tunnel für Mobile Daten setzen</string> <string name="mobile_data_tunnel">Als Tunnel für Mobile Daten setzen</string>
<string name="general">Allgemein</string>
<string name="restart_on_ping">Neustart bei PING Fehler (Beta)</string> <string name="restart_on_ping">Neustart bei PING Fehler (Beta)</string>
<string name="edit_tunnel">Tunnel bearbeiten</string> <string name="edit_tunnel">Tunnel bearbeiten</string>
<string name="set_primary_tunnel">Als Primären Tunnel setzen</string> <string name="set_primary_tunnel">Als Primären Tunnel setzen</string>
<string name="auto_on">Auto-Tunneln fortsetzen</string>
<string name="vpn_channel_id">VPN Kanal</string> <string name="vpn_channel_id">VPN Kanal</string>
<string name="vpn_channel_name">VPN Benachrichtigungskanal</string> <string name="vpn_channel_name">VPN Benachrichtigungskanal</string>
<string name="watcher_channel_name">Wächterbenachrichtigungskanal</string> <string name="watcher_channel_name">Wächterbenachrichtigungskanal</string>
@@ -104,38 +150,20 @@
<string name="kernel">Kernel</string> <string name="kernel">Kernel</string>
<string name="use_kernel">Kernelmodul verwenden</string> <string name="use_kernel">Kernelmodul verwenden</string>
<string name="error_ssid_exists">SSID existiert bereits</string> <string name="error_ssid_exists">SSID existiert bereits</string>
<string name="use_amnezia">"Amnezia Benutzerumgebung benutzen "</string>
<string name="junk_packet_count">Junk-Paket Anzahl</string> <string name="junk_packet_count">Junk-Paket Anzahl</string>
<string name="junk_packet_maximum_size">Junk-Paket maximale Grösse</string> <string name="junk_packet_maximum_size">Junk-Paket maximale Grösse</string>
<string name="init_packet_junk_size">Initial Junk-Paketgröße</string> <string name="init_packet_junk_size">Initial Junk-Paketgröße</string>
<string name="backend">Backend</string>
<string name="junk_packet_minimum_size">Junk-Paket minimale Grösse</string> <string name="junk_packet_minimum_size">Junk-Paket minimale Grösse</string>
<string name="response_packet_junk_size">Antwort Junk-Paketgröße</string> <string name="response_packet_junk_size">Antwort Junk-Paketgröße</string>
<string name="init_packet_magic_header">Initialpaket magic header</string> <string name="init_packet_magic_header">Initialpaket magic header</string>
<string name="getting_started_guide">Schnellstartanleitung</string> <string name="getting_started_guide">Startanleitung erhalten</string>
<string name="transport_packet_magic_header">Transportpaket magic header</string> <string name="transport_packet_magic_header">Transportpaket magic header</string>
<string name="underload_packet_magic_header">Unterlastpaket magic header</string> <string name="underload_packet_magic_header">Unterlastpaket magic header</string>
<string name="see_the">wirf einen Blick in die</string> <string name="see_the">Schaue das</string>
<string name="unsure_how">Wenn du nicht sicher bist, wie du weiterverfahren sollst</string> <string name="unsure_how">Wenn du nicht sicher bist, wie du weiterverfahren sollst</string>
<string name="error_file_format">Ungültiges Format der Tunnelkonfiguration</string> <string name="export_configs_failed">Konfigurationsexport fehlgeschlagen</string>
<string name="error_file_format">Ungültige Konfiguration Tunnel-Format</string>
<string name="response_packet_magic_header">Antwortpaket magic header</string> <string name="response_packet_magic_header">Antwortpaket magic header</string>
<string name="restart_at_boot">Beim Hochfahren starten</string>
<string name="optional_default">"optional, standard: "</string>
<string name="background_location_message">Für diese Funktion ist die Erlaubnis, den Standort jederzeit zu bestimmen, und/oder ein genauer Standort erforderlich. Bitte prüfe die</string>
<string name="vpn_settings">VPN-Systemeinstellungen</string>
<string name="always_on_message">Die Genehmigung für eine VPN-Verbindung wurde verweigert. Bitte überprüfe die</string>
<string name="chat_description">Tritt der Community bei</string>
<string name="set_custom_ping_cooldown">Ping-Neustart-Cooldown (sek)</string>
<string name="set_custom_ping_ip">Benutzerdefinierte Ping-IP einstellen</string>
<string name="default_ping_ip">(optional, Vorgabe ist peers)</string>
<string name="set_custom_ping_internal">Pingintervall (sek)</string>
<string name="always_on_message2">um sicherzustellen, dass Always-on VPN für alle anderen Apps ausgeschaltet ist und versuche es erneut</string>
<string name="tunnel_required">Feature erfordert mindestens einen Tunnel</string>
<string name="sec">Sek</string>
<string name="app_settings">App-Einstellungen</string>
<string name="background_location_message2">um sicherzustellen, dass diese Berechtigungen aktiviert sind.</string>
<string name="root_accepted">Root-Shell akzeptiert</string>
<string name="show_amnezia_properties">Amnezia-Eigenschaften anzeigen</string>
<string name="never">nie</string>
<string name="handshake">Handshake</string>
<string name="vpn_denied_dialog_title">Genehmigung verweigert</string>
<string name="logs">Logs</string>
</resources> </resources>
+50 -1
View File
@@ -1,33 +1,47 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="map">Mapa</string>
<string name="allowed_ips">Allowed IPs</string> <string name="allowed_ips">Allowed IPs</string>
<string name="email_chooser">Enviar un email…</string> <string name="email_chooser">Enviar un email…</string>
<string name="go">ir</string>
<string name="kernel">Kernel</string> <string name="kernel">Kernel</string>
<string name="restart">Reiniciar túnel</string>
<string name="vpn_connection_failed">Error de conexión</string>
<string name="error_none">Sin error</string>
<string name="other">Otros</string>
<string name="add_peer">Añadir peer</string> <string name="add_peer">Añadir peer</string>
<string name="done">Hecho</string> <string name="done">Hecho</string>
<string name="copy_public_key">Copiar public key</string> <string name="copy_public_key">Copiar public key</string>
<string name="base64_key">clave base64</string> <string name="base64_key">clave base64</string>
<string name="exported_configs_message">Configuración exportada a Descargas</string>
<string name="tunnel_on_wifi">Túnel en Wi-Fi no de confianza</string> <string name="tunnel_on_wifi">Túnel en Wi-Fi no de confianza</string>
<string name="location_services_missing_message">La app no detecta activado el servicio de ubicación en tu dispositivo. Dependiendo del dispositivo, esto podría hacer que la característica de Wi-Fi no de confianza falle al leer el nombre Wi-Fi. ¿Quieres continuar de todas formas?</string> <string name="location_services_missing_message">La app no detecta activado el servicio de ubicación en tu dispositivo. Dependiendo del dispositivo, esto podría hacer que la característica de Wi-Fi no de confianza falle al leer el nombre Wi-Fi. ¿Quieres continuar de todas formas?</string>
<string name="excluded">excluida(s)</string> <string name="excluded">excluida(s)</string>
<string name="all">todas</string> <string name="all">todas</string>
<string name="mobile_data_tunnel">Establecer como túnel en datos móviles</string> <string name="mobile_data_tunnel">Establecer como túnel en datos móviles</string>
<string name="use_tunnel_on_wifi_name">Usar tunnel en nombre Wi-Fi</string> <string name="use_tunnel_on_wifi_name">Usar tunnel en nombre Wi-Fi</string>
<string name="disabled">desactivado</string>
<string name="version">Versión</string> <string name="version">Versión</string>
<string name="userspace">Espacio del usuario</string>
<string name="mode">Modo</string>
<string name="support">Ayuda</string> <string name="support">Ayuda</string>
<string name="private_key">Clave privada</string> <string name="private_key">Clave privada</string>
<string name="trusted_ssid_value_description">Enviar SSID</string> <string name="trusted_ssid_value_description">Enviar SSID</string>
<string name="trusted_ssid_empty_description">Introducir SSID</string>
<string name="add_tunnels_text">Añadir desde archivo o zip</string> <string name="add_tunnels_text">Añadir desde archivo o zip</string>
<string name="open_file">Abrir archivo</string> <string name="open_file">Abrir archivo</string>
<string name="add_from_qr">Añadir mediante código QR</string> <string name="add_from_qr">Añadir mediante código QR</string>
<string name="qr_scan">Escanear QR</string> <string name="qr_scan">Escanear QR</string>
<string name="tunnel_name">Nombre de túnel</string> <string name="tunnel_name">Nombre de túnel</string>
<string name="add_tunnel">Añadir túnel</string>
<string name="exclude">Excluir</string> <string name="exclude">Excluir</string>
<string name="include">Incluir</string> <string name="include">Incluir</string>
<string name="tunnel_all">Todas las apps por el túnel</string> <string name="tunnel_all">Todas las apps por el túnel</string>
<string name="config_changes_saved">Cambios de configuración guardados.</string> <string name="config_changes_saved">Cambios de configuración guardados.</string>
<string name="save_changes">Guardar</string> <string name="save_changes">Guardar</string>
<string name="icon">Icono</string> <string name="icon">Icono</string>
<string name="no_thanks">No gracias</string>
<string name="turn_on">Activar</string>
<string name="mtu">MTU</string> <string name="mtu">MTU</string>
<string name="dns_servers">DNS servers</string> <string name="dns_servers">DNS servers</string>
<string name="addresses">Addresses</string> <string name="addresses">Addresses</string>
@@ -35,9 +49,17 @@
<string name="error_file_extension">No es un archivo .conf o .zip</string> <string name="error_file_extension">No es un archivo .conf o .zip</string>
<string name="turn_off_tunnel">Desactiva antes el túnel</string> <string name="turn_off_tunnel">Desactiva antes el túnel</string>
<string name="no_tunnels">¡Ningún túnel añadido aún!</string> <string name="no_tunnels">¡Ningún túnel añadido aún!</string>
<string name="watcher_notification_text_paused">Monitoreando cambios de red: En pausa</string>
<string name="watcher_notification_text_active">Monitoreando cambios de red: Activado</string>
<string name="tunnel_start_title">VPN conectada</string>
<string name="tunnel_start_text">Conectado al túnel</string>
<string name="notification_permission_required">Necesita permiso de notificaciones.</string>
<string name="add_trusted_ssid">Añadir nombres Wi-Fi de confianza</string>
<string name="tunnels">Túneles</string> <string name="tunnels">Túneles</string>
<string name="enable_auto_tunnel">Iniciar túnel-automático</string>
<string name="disable_auto_tunnel">Parar túnel-automático</string>
<string name="tunnel_mobile_data">Activar túnel en datos móviles</string> <string name="tunnel_mobile_data">Activar túnel en datos móviles</string>
<string name="privacy_policy">Ver Política de Privacidad</string> <string name="privacy_policy">Ver Política de Privacidad</string>
<string name="okay">OK</string> <string name="okay">OK</string>
<string name="tunnel_on_ethernet">Túnel en ethernet</string> <string name="tunnel_on_ethernet">Túnel en ethernet</string>
<string name="prominent_background_location_title">Divulgación de la ubicación en segundo plano</string> <string name="prominent_background_location_title">Divulgación de la ubicación en segundo plano</string>
@@ -48,11 +70,18 @@
<string name="always_on_vpn_support">Permitir VPN siempre-activada</string> <string name="always_on_vpn_support">Permitir VPN siempre-activada</string>
<string name="location_services_not_detected">Servicios de Ubicación No Detectados</string> <string name="location_services_not_detected">Servicios de Ubicación No Detectados</string>
<string name="hint_search_packages">Buscar paquetes</string> <string name="hint_search_packages">Buscar paquetes</string>
<string name="attempt_connection">Intentando conexión...</string>
<string name="vpn_starting">Iniciando VPN</string>
<string name="db_name">wg-tunnel-db</string> <string name="db_name">wg-tunnel-db</string>
<string name="scanning_qr">Escaneando QR</string>
<string name="none">Sin nombres Wi-Fi de confianza</string>
<string name="auto_tunneling">Túnel-automático</string> <string name="auto_tunneling">Túnel-automático</string>
<string name="vpn_on">VPN on</string> <string name="vpn_on">VPN on</string>
<string name="vpn_off">VPN off</string> <string name="vpn_off">VPN off</string>
<string name="default_vpn_on">VPN Principal on</string>
<string name="default_vpn_off">VPN Principal off</string>
<string name="create_import">Crear desde cero</string> <string name="create_import">Crear desde cero</string>
<string name="turn_off_auto">La acción necesita que túnel-automático esté desactivado o en pausa</string>
<string name="turn_on_tunnel">La acción necesita un túnel activado</string> <string name="turn_on_tunnel">La acción necesita un túnel activado</string>
<string name="rotate_keys">Rotar claves</string> <string name="rotate_keys">Rotar claves</string>
<string name="comma_separated_list">lista separada por comas</string> <string name="comma_separated_list">lista separada por comas</string>
@@ -65,6 +94,9 @@
<string name="error_authorization_failed">Fallo de autorización</string> <string name="error_authorization_failed">Fallo de autorización</string>
<string name="enabled_app_shortcuts">Habilitar acesos directos de app</string> <string name="enabled_app_shortcuts">Habilitar acesos directos de app</string>
<string name="export_configs">Exportar configuración</string> <string name="export_configs">Exportar configuración</string>
<string name="background_location_required">Se necesita ubicación en segundo plano</string>
<string name="location_services_required">Se necesita servicio de ubicación</string>
<string name="precise_location_required">Necesita ubicación precisa</string>
<string name="unknown_error">Error desconocido</string> <string name="unknown_error">Error desconocido</string>
<string name="email_subject">Ayuda WG Tunnel</string> <string name="email_subject">Ayuda WG Tunnel</string>
<string name="interface_">Interfaz</string> <string name="interface_">Interfaz</string>
@@ -73,6 +105,7 @@
<string name="persistent_keepalive">Keepalive persistente</string> <string name="persistent_keepalive">Keepalive persistente</string>
<string name="docs_description">Leer documentación</string> <string name="docs_description">Leer documentación</string>
<string name="email_description">Envíame un email</string> <string name="email_description">Envíame un email</string>
<string name="support_help_text">Si tienes problemas, ideas para mejoras, o simlemente comprometerte, tienes disponibles los siguientes recursos:</string>
<string name="use_kernel">Usar módulo del Kernel</string> <string name="use_kernel">Usar módulo del Kernel</string>
<string name="error_ssid_exists">SSID existente</string> <string name="error_ssid_exists">SSID existente</string>
<string name="error_root_denied">Shell root denegado</string> <string name="error_root_denied">Shell root denegado</string>
@@ -82,13 +115,20 @@
<string name="delete_tunnel">Eliminar túnel</string> <string name="delete_tunnel">Eliminar túnel</string>
<string name="delete_tunnel_message">¿Estás seguro de que quieres eliminar este túnel?</string> <string name="delete_tunnel_message">¿Estás seguro de que quieres eliminar este túnel?</string>
<string name="yes"></string> <string name="yes"></string>
<string name="resume">Reanudar</string>
<string name="pause">Pausar</string>
<string name="active">activado</string>
<string name="paused">en pausa</string>
<string name="tunneling_apps">Apps por el túnel</string> <string name="tunneling_apps">Apps por el túnel</string>
<string name="included">incluida(s)</string> <string name="included">incluida(s)</string>
<string name="always_on_disabled">VPN siempre-activada ha intentado iniciar un túnel, pero está característica está deshabilitada en los ajustes.</string>
<string name="no_email_detected">Ninguna app de email detectada</string> <string name="no_email_detected">Ninguna app de email detectada</string>
<string name="no_browser_detected">Ningún navegador detectado</string> <string name="no_browser_detected">Ningún navegador detectado</string>
<string name="logs_saved">Registros guardados en Descargas</string>
<string name="open_issue">Abrir una incidencia</string> <string name="open_issue">Abrir una incidencia</string>
<string name="read_logs">Leer los registros</string> <string name="read_logs">Leer los registros</string>
<string name="auto">(automático)</string> <string name="auto">(automático)</string>
<string name="config_parse_error">Fallo al analizar la configuración</string>
<string name="incorrect_pin">El pin no es correcto</string> <string name="incorrect_pin">El pin no es correcto</string>
<string name="pin_created">Pin creado con éxito</string> <string name="pin_created">Pin creado con éxito</string>
<string name="enter_pin">Introduce tu pin</string> <string name="enter_pin">Introduce tu pin</string>
@@ -96,7 +136,13 @@
<string name="enable_app_lock">Activar el bloqueo de aplicaciones</string> <string name="enable_app_lock">Activar el bloqueo de aplicaciones</string>
<string name="restart_on_ping">Reiniciar al fallar ping (beta)</string> <string name="restart_on_ping">Reiniciar al fallar ping (beta)</string>
<string name="set_primary_tunnel">Establecer como túnel Principal</string> <string name="set_primary_tunnel">Establecer como túnel Principal</string>
<string name="no_wifi_names_configured">No hay nombres Wi-Fi configurados para este túnel</string>
<string name="general">General</string>
<string name="edit_tunnel">Editar túnel</string> <string name="edit_tunnel">Editar túnel</string>
<string name="auto_on">Reanudar túnel-automático</string>
<string name="auto_off">Pausar túnel-automático</string>
<string name="auto_tun_on">Reanudar el túnel automático</string>
<string name="auto_tun_off">Pausa del túnel automático</string>
<string name="settings">Ajustes</string> <string name="settings">Ajustes</string>
<string name="app_name">WG Tunnel</string> <string name="app_name">WG Tunnel</string>
<string name="vpn_channel_id">Canal VPN</string> <string name="vpn_channel_id">Canal VPN</string>
@@ -104,6 +150,9 @@
<string name="watcher_channel_id">Canal del obvervador</string> <string name="watcher_channel_id">Canal del obvervador</string>
<string name="watcher_channel_name">Canal de notificación del obvervador</string> <string name="watcher_channel_name">Canal de notificación del obvervador</string>
<string name="prominent_background_location_message">La monitorización SSID Wi-Fi necesita de permiso de ubicación en segundo plano incluso si la app está cerrada. Mira el enlace a la Política de Privacidad en la pantalla de ayuda para más detalles.</string> <string name="prominent_background_location_message">La monitorización SSID Wi-Fi necesita de permiso de ubicación en segundo plano incluso si la app está cerrada. Mira el enlace a la Política de Privacidad en la pantalla de ayuda para más detalles.</string>
<string name="export_configs_failed">Error al exportar la configuración</string>
<string name="use_amnezia">"Utilizar el entorno de usuario de Amnezia "</string>
<string name="junk_packet_count">Recuento de paquetes basura</string> <string name="junk_packet_count">Recuento de paquetes basura</string>
<string name="backend">Backend</string>
<string name="junk_packet_minimum_size">Tamaño mínimo del paquete basura</string> <string name="junk_packet_minimum_size">Tamaño mínimo del paquete basura</string>
</resources> </resources>

Some files were not shown because too many files have changed in this diff Show More