Compare commits

..

7 Commits

Author SHA1 Message Date
Zane Schepke 5946d7c10d feat: add lock, logs, and ping
Fixes bug where control tile tunnel did not match with tunnel being controlled Closes #132
Fixes tunnel config edit screen error message #131

Revert to official lib to fix slow speeds issue Closes #137

Adds local app lock feature Closes #88

Adds restart vpn on ping fail with 1 minute interval and 60 minute cooldown Closes #6

Adds ability to easily make a copy of a tunnel.

Fixes bug on AndroidTV where tunnels were not being deleted properly.

Fixes bug where auto tunneling could be turned on before VPN permission was given.
2024-03-18 22:52:00 -04:00
Zane Schepke 4fc8ffbcbb feat: add logs screen 2024-03-13 02:52:57 -04:00
Zane Schepke c0cff297b2 remove firebase 2024-03-11 12:39:00 -04:00
Zane Schepke ee8db0a859 fix: ui bug and graphene notification
Fixes a bug where save button was hidden on config screen

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

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

Closes #116
2024-02-19 08:45:52 -05:00
Zane Schepke 3247e94358 Merge pull request #105 from zaneschepke/dependabot/github_actions/actions/upload-artifact-4.3.1
build(deps): bump actions/upload-artifact from 4.3.0 to 4.3.1
2024-02-19 06:59:32 -05:00
dependabot[bot] 84b2b75271 build(deps): bump actions/upload-artifact from 4.3.0 to 4.3.1
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.3.0 to 4.3.1.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4.3.0...v4.3.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-06 14:02:45 +00:00
65 changed files with 1713 additions and 793 deletions
+1 -1
View File
@@ -68,7 +68,7 @@ jobs:
echo "VERSION_CODE=$version_code" >> $GITHUB_ENV
# Save the APK after the Build job is complete to publish it as a Github release in the next job
- name: Upload APK
uses: actions/upload-artifact@v4.3.0
uses: actions/upload-artifact@v4.3.1
with:
name: wgtunnel
path: ${{ steps.apk-path.outputs.path }}
+1 -2
View File
@@ -69,7 +69,7 @@ jobs:
echo "VERSION_CODE=$version_code" >> $GITHUB_ENV
# Save the APK after the Build job is complete to publish it as a Github release in the next job
- name: Upload APK
uses: actions/upload-artifact@v4.3.0
uses: actions/upload-artifact@v4.3.1
with:
name: wgtunnel
path: ${{ steps.apk-path.outputs.path }}
@@ -89,7 +89,6 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
# fix hardcode changelog file name
body_path: ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/${{ env.VERSION_CODE }}.txt
tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }}
+7 -9
View File
@@ -4,7 +4,7 @@ plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.hilt.android)
id("org.jetbrains.kotlin.plugin.serialization")
alias(libs.plugins.kotlinxSerialization)
alias(libs.plugins.ksp)
}
@@ -111,8 +111,7 @@ android {
create("general") {
dimension = Constants.TYPE
if (BuildHelper.isReleaseBuild(gradle) && BuildHelper.isGeneralFlavor(gradle)) {
apply(plugin = "com.google.gms.google-services")
apply(plugin = "com.google.firebase.crashlytics")
//any plugins general specific
}
}
}
@@ -133,9 +132,12 @@ android {
val generalImplementation by configurations
dependencies {
implementation(project(":logcatter"))
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
// optional - helpers for implementing LifecycleOwner in a Service
// helpers for implementing LifecycleOwner in a Service
implementation(libs.androidx.lifecycle.service)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
@@ -193,17 +195,13 @@ dependencies {
// serialization
implementation(libs.kotlinx.serialization.json)
// firebase crashlytics
generalImplementation(platform(libs.firebase.bom))
generalImplementation(libs.google.firebase.crashlytics.ktx)
generalImplementation(libs.google.firebase.analytics.ktx)
// barcode scanning
implementation(libs.zxing.android.embedded)
implementation(libs.zxing.core)
// bio
implementation(libs.androidx.biometric.ktx)
implementation(libs.pin.lock.compose)
// shortcuts
implementation(libs.androidx.core)
-39
View File
@@ -1,39 +0,0 @@
{
"project_info": {
"project_number": "328300975830",
"project_id": "wireguard-auto-tunnel",
"storage_bucket": "wireguard-auto-tunnel.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:328300975830:android:82cd774598ccb7234b1b77",
"android_client_info": {
"package_name": "com.zaneschepke.wireguardautotunnel"
}
},
"oauth_client": [
{
"client_id": "328300975830-m72lc3hr69ddhdqh9ngr27rvc8o0jb2d.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyBsSMY0LlckizXDnuYBy7nXWGSdl8zZedI"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "328300975830-m72lc3hr69ddhdqh9ngr27rvc8o0jb2d.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}
],
"configuration_version": "1"
}
@@ -0,0 +1,168 @@
{
"formatVersion": 1,
"database": {
"version": 6,
"identityHash": "625820076477aca948536f7bccccc7ca",
"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, `default_tunnel` TEXT, `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_battery_saver_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_auto_tunnel_paused` INTEGER NOT NULL DEFAULT false, `is_ping_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": "defaultTunnel",
"columnName": "default_tunnel",
"affinity": "TEXT",
"notNull": false
},
{
"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": "isBatterySaverEnabled",
"columnName": "is_battery_saver_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": "isAutoTunnelPaused",
"columnName": "is_auto_tunnel_paused",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_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)",
"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
}
],
"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, '625820076477aca948536f7bccccc7ca')"
]
}
}
+5 -7
View File
@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
@@ -23,7 +22,6 @@
<!--foreground service exempt android 14-->
<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.FOREGROUND_SERVICE_SPECIAL_USE" />
<!--foreground service permissions-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
@@ -84,7 +82,8 @@
android:screenOrientation="fullSensor"
android:stateNotNeeded="true"
android:theme="@style/zxing_CaptureTheme"
android:windowSoftInputMode="stateAlwaysHidden" />
android:windowSoftInputMode="stateAlwaysHidden"
tools:ignore="DiscouragedApi" />
<activity
android:name=".service.shortcut.ShortcutsActivity"
android:enabled="true"
@@ -96,7 +95,7 @@
android:name=".service.foreground.ForegroundService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="systemExempted|specialUse"
android:foregroundServiceType="systemExempted"
tools:node="merge" />
<service
android:name=".service.tile.TunnelControlTile"
@@ -119,7 +118,7 @@
android:name=".service.foreground.WireGuardTunnelService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="systemExempted|specialUse"
android:foregroundServiceType="systemExempted"
android:permission="android.permission.BIND_VPN_SERVICE"
android:persistent="true"
tools:node="merge">
@@ -134,7 +133,7 @@
android:name=".service.foreground.WireGuardConnectivityWatcherService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="systemExempted|specialUse"
android:foregroundServiceType="systemExempted"
android:persistent="true"
android:stopWithTask="false"
tools:node="merge" />
@@ -145,7 +144,6 @@
android:exported="false">
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.ACTION_BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
@@ -2,20 +2,23 @@ package com.zaneschepke.wireguardautotunnel
import android.app.Application
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import android.service.quicksettings.TileService
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber
import xyz.teamgravity.pin_lock_compose.PinManager
@HiltAndroidApp
class WireGuardAutoTunnel : Application() {
override fun onCreate() {
super.onCreate()
instance = this
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) else Timber.plant(ReleaseTree())
PinManager.initialize(this)
}
companion object {
lateinit var instance: WireGuardAutoTunnel
private set
@@ -24,9 +27,9 @@ class WireGuardAutoTunnel : Application() {
return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
}
fun requestTileServiceStateUpdate() {
fun requestTileServiceStateUpdate(context : Context) {
TileService.requestListeningState(
instance,
context,
ComponentName(instance, TunnelControlTile::class.java),
)
}
@@ -9,7 +9,7 @@ import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
@Database(
entities = [Settings::class, TunnelConfig::class],
version = 5,
version = 6,
autoMigrations =
[
AutoMigration(from = 1, to = 2),
@@ -22,6 +22,10 @@ import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
from = 4,
to = 5,
),
AutoMigration(
from = 5,
to = 6,
),
],
exportSchema = true,
)
@@ -18,7 +18,7 @@ object Queries {
VALUES
('false',
'false',
'[trustedSSID1,trustedSSID2]',
'sampleSSID1,sampleSSID2',
NULL,
'false',
'false',
@@ -8,6 +8,7 @@ import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
class DataStoreManager(private val context: Context) {
companion object {
@@ -32,7 +33,11 @@ class DataStoreManager(private val context: Context) {
fun <T> getFromStoreFlow(key: Preferences.Key<T>) = context.dataStore.data.map { it[key] }
suspend fun <T> getFromStore(key: Preferences.Key<T>) =
context.dataStore.data.first { it.contains(key) }[key]
context.dataStore.data.map{ it[key] }.first()
fun <T> getFromStoreBlocking(key: Preferences.Key<T>) = runBlocking {
context.dataStore.data.map{ it[key] }.first()
}
val preferencesFlow: Flow<Preferences?> = context.dataStore.data
}
@@ -51,6 +51,11 @@ data class Settings(
defaultValue = "false",
)
var isAutoTunnelPaused: Boolean = false,
@ColumnInfo(
name = "is_ping_enabled",
defaultValue = "false",
)
var isPingEnabled: Boolean = false,
) {
fun isTunnelConfigDefault(tunnelConfig: TunnelConfig): Boolean {
return if (defaultTunnel != null) {
@@ -56,7 +56,7 @@ open class ForegroundService : LifecycleService() {
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
} catch (e: Exception) {
Timber.d("Service stopped without being started: ${e.message}")
Timber.e(e)
}
isServiceStarted = false
}
@@ -1,12 +1,8 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.PowerManager
import android.os.SystemClock
import androidx.core.app.ServiceCompat
import androidx.lifecycle.lifecycleScope
import com.wireguard.android.backend.Tunnel
@@ -28,10 +24,11 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.net.InetAddress
import javax.inject.Inject
@AndroidEntryPoint
class WireGuardConnectivityWatcherService : ForegroundService() {
private val foregroundId = 122
@@ -122,49 +119,18 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
launchWatcherNotification(getString(R.string.watcher_notification_text_paused))
}
// TODO could this be restarting service in a bad state?
// try to start task again if killed
override fun onTaskRemoved(rootIntent: Intent) {
Timber.d("Task Removed called")
val restartServiceIntent = Intent(rootIntent)
val restartServicePendingIntent: PendingIntent =
PendingIntent.getService(
this,
1,
restartServiceIntent,
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE,
)
applicationContext.getSystemService(Context.ALARM_SERVICE)
val alarmService: AlarmManager =
applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmService.set(
AlarmManager.ELAPSED_REALTIME,
SystemClock.elapsedRealtime() + 1000,
restartServicePendingIntent,
)
}
private suspend fun initWakeLock() {
val isBatterySaverOn =
withContext(lifecycleScope.coroutineContext) {
settingsRepository.getSettings().isBatterySaverEnabled
}
private fun initWakeLock() {
wakeLock =
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
try {
if (isBatterySaverOn) {
Timber.d("Initiating wakelock with timeout")
acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT)
} else {
Timber.d("Initiating wakelock with zero timeout")
acquire(Constants.DEFAULT_WATCHER_WAKE_LOCK_TIMEOUT)
}
} finally {
release()
}
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
try {
Timber.i("Initiating wakelock with 10 min timeout")
acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT)
} finally {
release()
}
}
}
}
private fun cancelWatcherJob() {
@@ -178,33 +144,40 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
lifecycleScope.launch(Dispatchers.IO) {
val setting = settingsRepository.getSettings()
launch {
Timber.d("Starting wifi watcher")
Timber.i("Starting wifi watcher")
watchForWifiConnectivityChanges()
}
if (setting.isTunnelOnMobileDataEnabled) {
launch {
Timber.d("Starting mobile data watcher")
Timber.i("Starting mobile data watcher")
watchForMobileDataConnectivityChanges()
}
}
if (setting.isTunnelOnEthernetEnabled) {
launch {
Timber.d("Starting ethernet data watcher")
Timber.i("Starting ethernet data watcher")
watchForEthernetConnectivityChanges()
}
}
launch {
Timber.d("Starting vpn state watcher")
Timber.i("Starting vpn state watcher")
watchForVpnConnectivityChanges()
}
launch {
Timber.d("Starting settings watcher")
Timber.i("Starting settings watcher")
watchForSettingsChanges()
}
if(setting.isPingEnabled) {
launch {
Timber.i("Starting ping watcher")
watchForPingFailure()
}
}
launch {
Timber.d("Starting management watcher")
Timber.i("Starting management watcher")
manageVpn()
}
}
}
@@ -212,7 +185,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
mobileDataService.networkStatus.collect {
when (it) {
is NetworkStatus.Available -> {
Timber.d("Gained Mobile data connection")
Timber.i("Gained Mobile data connection")
networkEventsFlow.value =
networkEventsFlow.value.copy(
isMobileDataConnected = true,
@@ -223,19 +196,53 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
networkEventsFlow.value.copy(
isMobileDataConnected = true,
)
Timber.d("Mobile data capabilities changed")
Timber.i("Mobile data capabilities changed")
}
is NetworkStatus.Unavailable -> {
networkEventsFlow.value =
networkEventsFlow.value.copy(
isMobileDataConnected = false,
)
Timber.d("Lost mobile data connection")
Timber.i("Lost mobile data connection")
}
}
}
}
private suspend fun watchForPingFailure() {
try {
do {
if(vpnService.vpnState.value.status == Tunnel.State.UP) {
val config = vpnService.vpnState.value.config
config?.let {
val results = it.peers.map { peer ->
val host = if(peer.endpoint.isPresent &&
peer.endpoint.get().resolved.isPresent)
peer.endpoint.get().resolved.get().host
else Constants.BACKUP_PING_HOST
Timber.i("Checking reachability of: $host")
val reachable = InetAddress.getByName(host).isReachable(Constants.PING_TIMEOUT.toInt())
Timber.i("Result: reachable - $reachable")
reachable
}
if(results.contains(false)) {
Timber.i("Restarting VPN for ping failure")
ServiceManager.stopVpnService(this)
delay(Constants.VPN_RESTART_DELAY)
val tunnel = networkEventsFlow.value.settings.defaultTunnel
ServiceManager.startVpnServiceForeground(this, tunnel!!)
delay(Constants.PING_COOLDOWN)
}
}
}
delay(Constants.PING_INTERVAL)
} while (true)
} catch (e: Exception) {
Timber.e(e)
}
}
private suspend fun watchForSettingsChanges() {
settingsRepository.getSettingsFlow().collect {
if (networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) {
@@ -273,14 +280,14 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
ethernetService.networkStatus.collect {
when (it) {
is NetworkStatus.Available -> {
Timber.d("Gained Ethernet connection")
Timber.i("Gained Ethernet connection")
networkEventsFlow.value =
networkEventsFlow.value.copy(
isEthernetConnected = true,
)
}
is NetworkStatus.CapabilitiesChanged -> {
Timber.d("Ethernet capabilities changed")
Timber.i("Ethernet capabilities changed")
networkEventsFlow.value =
networkEventsFlow.value.copy(
isEthernetConnected = true,
@@ -291,7 +298,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
networkEventsFlow.value.copy(
isEthernetConnected = false,
)
Timber.d("Lost Ethernet connection")
Timber.i("Lost Ethernet connection")
}
}
}
@@ -301,20 +308,20 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
wifiService.networkStatus.collect {
when (it) {
is NetworkStatus.Available -> {
Timber.d("Gained Wi-Fi connection")
Timber.i("Gained Wi-Fi connection")
networkEventsFlow.value =
networkEventsFlow.value.copy(
isWifiConnected = true,
)
}
is NetworkStatus.CapabilitiesChanged -> {
Timber.d("Wifi capabilities changed")
Timber.i("Wifi capabilities changed")
networkEventsFlow.value =
networkEventsFlow.value.copy(
isWifiConnected = true,
)
val ssid = wifiService.getNetworkName(it.networkCapabilities) ?: ""
Timber.d("Detected SSID: $ssid")
Timber.i("Detected SSID: $ssid")
networkEventsFlow.value =
networkEventsFlow.value.copy(
currentNetworkSSID = ssid,
@@ -325,16 +332,15 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
networkEventsFlow.value.copy(
isWifiConnected = false,
)
Timber.d("Lost Wi-Fi connection")
Timber.i("Lost Wi-Fi connection")
}
}
}
}
// TODO clean this up
private suspend fun manageVpn() {
networkEventsFlow.collectLatest {
Timber.i("New watcher state: $it")
val autoTunnel = "Auto-tunnel watcher"
if (!it.settings.isAutoTunnelPaused && it.settings.defaultTunnel != null) {
delay(Constants.TOGGLE_TUNNEL_DELAY)
when {
@@ -342,7 +348,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
it.settings.isTunnelOnEthernetEnabled &&
!it.isVpnConnected)) -> {
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
Timber.i("Condition 1 met")
Timber.i("$autoTunnel condition 1 met")
}
(!it.isEthernetConnected &&
it.settings.isTunnelOnMobileDataEnabled &&
@@ -350,14 +356,14 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
it.isMobileDataConnected &&
!it.isVpnConnected) -> {
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
Timber.i("Condition 2 met")
Timber.i("$autoTunnel condition 2 met")
}
(!it.isEthernetConnected &&
!it.settings.isTunnelOnMobileDataEnabled &&
!it.isWifiConnected &&
it.isVpnConnected) -> {
ServiceManager.stopVpnService(this)
Timber.i("Condition 3 met")
Timber.i("$autoTunnel condition 3 met")
}
(!it.isEthernetConnected &&
it.isWifiConnected &&
@@ -365,31 +371,31 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
it.settings.isTunnelOnWifiEnabled &&
(!it.isVpnConnected)) -> {
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
Timber.i("Condition 4 met")
Timber.i("$autoTunnel condition 4 met")
}
(!it.isEthernetConnected &&
(it.isWifiConnected &&
it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID)) &&
(it.isVpnConnected)) -> {
ServiceManager.stopVpnService(this)
Timber.i("Condition 5 met")
Timber.i("$autoTunnel condition 5 met")
}
(!it.isEthernetConnected &&
(it.isWifiConnected &&
!it.settings.isTunnelOnWifiEnabled &&
(it.isVpnConnected))) -> {
ServiceManager.stopVpnService(this)
Timber.i("Condition 6 met")
Timber.i("$autoTunnel condition 6 met")
}
(!it.isEthernetConnected &&
!it.isWifiConnected &&
!it.isMobileDataConnected &&
(it.isVpnConnected)) -> {
ServiceManager.stopVpnService(this)
Timber.i("Condition 7 met")
Timber.i("$autoTunnel condition 7 met")
}
else -> {
Timber.i("No condition met")
Timber.i("$autoTunnel no condition met")
}
}
}
@@ -68,7 +68,7 @@ class WireGuardTunnelService : ForegroundService() {
stopService(extras)
}
} else {
Timber.d("Tunnel config null, starting default tunnel or first")
Timber.i("Tunnel config null, starting default tunnel or first")
val settings = settingsRepository.getSettings()
val tunnels = tunnelConfigRepository.getAll()
if (settings.isAlwaysOnVpnEnabled) {
@@ -84,6 +84,8 @@ class WireGuardTunnelService : ForegroundService() {
tunnelName = tunnel.name
vpnService.startTunnel(tunnel)
}
} else {
launchAlwaysOnDisabledNotification()
}
}
}
@@ -116,6 +118,11 @@ class WireGuardTunnelService : ForegroundService() {
}
}
private fun launchAlwaysOnDisabledNotification() {
launchVpnNotification(title = this.getString(R.string.vpn_connection_failed),
description = this.getString(R.string.always_on_disabled))
}
override fun stopService(extras: Bundle?) {
super.stopService(extras)
lifecycleScope.launch(Dispatchers.IO) {
@@ -45,8 +45,7 @@ class TunnelControlTile() : TileService() {
setUnavailable()
return@collect
}
tunnelName =
it.name.ifBlank {
tunnelName = it.name.run {
val settings = settingsRepository.getSettings()
if (settings.defaultTunnel != null) {
TunnelConfig.from(settings.defaultTunnel!!).name
@@ -72,15 +71,18 @@ class TunnelControlTile() : TileService() {
unlockAndRun {
scope.launch {
try {
val tunnelConfig =
tunnelConfigRepository.getAll().first { it.name == tunnelName }
val defaultTunnel = settingsRepository.getSettings().defaultTunnel
val config = defaultTunnel ?: run {
val tunnelConfigs = tunnelConfigRepository.getAll()
return@run tunnelConfigs.find { it.name == tunnelName }
}
toggleWatcherServicePause()
if (vpnService.getState() == Tunnel.State.UP) {
ServiceManager.stopVpnService(this@TunnelControlTile)
} else {
ServiceManager.startVpnServiceForeground(
this@TunnelControlTile,
tunnelConfig.toString(),
config.toString(),
)
}
} catch (e: Exception) {
@@ -2,9 +2,12 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel
import com.wireguard.android.backend.Statistics
import com.wireguard.android.backend.Tunnel
import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
data class VpnState(
val status: Tunnel.State = Tunnel.State.DOWN,
val name: String = "",
val config: Config? = null,
val statistics: Statistics? = null
)
@@ -63,6 +63,7 @@ constructor(
stopTunnelOnConfigChange(tunnelConfig)
emitTunnelName(tunnelConfig.name)
config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
emitTunnelConfig(config)
val state =
backend.setState(
this,
@@ -71,7 +72,7 @@ constructor(
)
emitTunnelState(state)
state
} catch (e: Exception) {
} catch (e: BackendException) {
Timber.e("Failed to start tunnel with error: ${e.message}")
State.DOWN
}
@@ -101,6 +102,14 @@ constructor(
)
}
private suspend fun emitTunnelConfig(config : Config?) {
_vpnState.emit(
_vpnState.value.copy(
config = config,
),
)
}
private suspend fun stopTunnelOnConfigChange(tunnelConfig: TunnelConfig) {
if (getState() == State.UP && _vpnState.value.name != tunnelConfig.name) {
stopTunnel()
@@ -129,7 +138,7 @@ constructor(
override fun onStateChange(state: State) {
val tunnel = this
emitTunnelState(state)
WireGuardAutoTunnel.requestTileServiceStateUpdate()
WireGuardAutoTunnel.requestTileServiceStateUpdate(WireGuardAutoTunnel.instance)
if (state == State.UP) {
statsJob =
scope.launch {
@@ -1,12 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class ActivityViewModel
@Inject
constructor() : ViewModel() {
}
@@ -0,0 +1,9 @@
package com.zaneschepke.wireguardautotunnel.ui
data class AppUiState(
val snackbarMessage: String = "",
val snackbarMessageConsumed: Boolean = true,
val vpnPermissionAccepted: Boolean = false,
val notificationPermissionAccepted: Boolean = false,
val requestPermissions: Boolean = false
)
@@ -0,0 +1,142 @@
package com.zaneschepke.wireguardautotunnel.ui
import android.app.Application
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wireguard.android.backend.GoBackend
import com.zaneschepke.logcatter.Logcatter
import com.zaneschepke.logcatter.model.LogMessage
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.FileUtils
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.time.Instant
import javax.inject.Inject
@HiltViewModel
class AppViewModel
@Inject
constructor(
private val application: Application,
) : ViewModel() {
val vpnIntent: Intent? = GoBackend.VpnService.prepare(WireGuardAutoTunnel.instance)
private val _appUiState = MutableStateFlow(AppUiState(
vpnPermissionAccepted = vpnIntent == null
))
val appUiState = _appUiState.asStateFlow()
fun isRequiredPermissionGranted() : Boolean {
val allAccepted = (_appUiState.value.vpnPermissionAccepted && _appUiState.value.vpnPermissionAccepted)
if(!allAccepted) requestPermissions()
return allAccepted
}
private fun requestPermissions() {
_appUiState.value = _appUiState.value.copy(
requestPermissions = true
)
}
fun permissionsRequested() {
_appUiState.value = _appUiState.value.copy(
requestPermissions = false
)
}
fun openWebPage(url: String) {
try {
val webpage: Uri = Uri.parse(url)
val intent = Intent(Intent.ACTION_VIEW, webpage).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
application.startActivity(intent)
} catch (e: ActivityNotFoundException) {
Timber.e(e)
showSnackbarMessage(application.getString(R.string.no_browser_detected))
}
}
fun onVpnPermissionAccepted() {
_appUiState.value = _appUiState.value.copy(
vpnPermissionAccepted = true
)
}
fun launchEmail() {
try {
val intent =
Intent(Intent.ACTION_SENDTO).apply {
type = Constants.EMAIL_MIME_TYPE
putExtra(Intent.EXTRA_EMAIL, arrayOf(application.getString(R.string.my_email)))
putExtra(Intent.EXTRA_SUBJECT, application.getString(R.string.email_subject))
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
application.startActivity(
Intent.createChooser(intent, application.getString(R.string.email_chooser)).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
})
} catch (e: ActivityNotFoundException) {
Timber.e(e)
showSnackbarMessage(application.getString(R.string.no_email_detected))
}
}
fun showSnackbarMessage(message : String) {
_appUiState.value = _appUiState.value.copy(
snackbarMessage = message,
snackbarMessageConsumed = false
)
}
fun snackbarMessageConsumed() {
_appUiState.value = _appUiState.value.copy(
snackbarMessage = "",
snackbarMessageConsumed = true
)
}
val logs = mutableStateListOf<LogMessage>()
fun readLogCatOutput() = viewModelScope.launch(viewModelScope.coroutineContext + Dispatchers.IO) {
launch {
Logcatter.logs {
logs.add(it)
if (logs.size > Constants.LOG_BUFFER_SIZE) {
logs.removeRange(0, (logs.size - Constants.LOG_BUFFER_SIZE).toInt())
}
}
}
}
fun clearLogs() {
logs.clear()
Logcatter.clear()
}
fun saveLogsToFile() {
val fileName = "${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.txt"
val content = logs.joinToString(separator = "\n")
FileUtils.saveFileToDownloads(application.applicationContext, content, fileName)
Toast.makeText(application, application.getString(R.string.logs_saved), Toast.LENGTH_SHORT).show()
}
fun setNotificationPermissionAccepted(accepted: Boolean) {
_appUiState.value = _appUiState.value.copy(
notificationPermissionAccepted = accepted
)
}
}
@@ -1,17 +1,16 @@
package com.zaneschepke.wireguardautotunnel.ui
import android.Manifest
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import androidx.activity.SystemBarStyle
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
@@ -20,18 +19,26 @@ import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Text
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
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.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
@@ -40,20 +47,22 @@ import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.ui.common.PermissionRequestFailedScreen
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.pinlock.PinLockScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.logs.LogsScreen
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.StringValue
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.IOException
import xyz.teamgravity.pin_lock_compose.PinLock
import xyz.teamgravity.pin_lock_compose.PinManager
import javax.inject.Inject
@AndroidEntryPoint
@@ -62,7 +71,9 @@ class MainActivity : AppCompatActivity() {
@Inject
lateinit var dataStoreManager: DataStoreManager
@Inject lateinit var settingsRepository: SettingsRepository
@Inject
lateinit var settingsRepository: SettingsRepository
@OptIn(
ExperimentalPermissionsApi::class,
)
@@ -73,51 +84,79 @@ class MainActivity : AppCompatActivity() {
// load preferences into memory and init data
lifecycleScope.launch {
try {
dataStoreManager.init()
WireGuardAutoTunnel.requestTileServiceStateUpdate()
} catch (e: IOException) {
Timber.e("Failed to load preferences")
dataStoreManager.init()
WireGuardAutoTunnel.requestTileServiceStateUpdate(this@MainActivity)
val settings = settingsRepository.getSettings()
if (settings.isAutoTunnelEnabled) {
ServiceManager.startWatcherService(application.applicationContext)
}
}
setContent {
//val activityViewModel = hiltViewModel<ActivityViewModel>()
val appViewModel = hiltViewModel<AppViewModel>()
val appUiState by appViewModel.appUiState.collectAsStateWithLifecycle()
val navController = rememberNavController()
val focusRequester = remember { FocusRequester() }
val navBackStackEntry by navController.currentBackStackEntryAsState()
WireguardAutoTunnelTheme {
val snackbarHostState = remember { SnackbarHostState() }
val notificationPermissionState = if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
val notificationPermissionState =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS) else null
fun requestNotificationPermission() {
if (notificationPermissionState != null && !notificationPermissionState.status.isGranted
) {
notificationPermissionState.launchPermissionRequest()
val focusRequester = remember { FocusRequester() }
val snackbarHostState = remember { SnackbarHostState() }
val vpnActivityResultState =
rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult(),
onResult = {
val accepted = (it.resultCode == RESULT_OK)
if (accepted) {
appViewModel.onVpnPermissionAccepted()
}
},
)
fun showSnackBarMessage(message: StringValue) {
lifecycleScope.launch(Dispatchers.Main) {
val result =
snackbarHostState.showSnackbar(
message = message.asString(this@MainActivity),
duration = SnackbarDuration.Short,
)
when (result) {
SnackbarResult.ActionPerformed,
SnackbarResult.Dismissed -> {
snackbarHostState.currentSnackbarData?.dismiss()
}
}
}
}
LaunchedEffect(appUiState.requestPermissions) {
if (appUiState.requestPermissions) {
appViewModel.permissionsRequested()
if (notificationPermissionState != null && !notificationPermissionState.status.isGranted
) {
showSnackBarMessage(StringValue.StringResource(R.string.notification_permission_required))
return@LaunchedEffect notificationPermissionState.launchPermissionRequest()
}
if (!appUiState.vpnPermissionAccepted) {
return@LaunchedEffect vpnActivityResultState.launch(appViewModel.vpnIntent)
}
}
}
WireguardAutoTunnelTheme {
LaunchedEffect(Unit) {
requestNotificationPermission()
appViewModel.setNotificationPermissionAccepted(
notificationPermissionState?.status?.isGranted ?: true,
)
if(!WireGuardAutoTunnel.isRunningOnAndroidTv()) appViewModel.readLogCatOutput()
}
fun showSnackBarMessage(message: String) {
lifecycleScope.launch(Dispatchers.Main) {
val result =
snackbarHostState.showSnackbar(
message = message,
actionLabel = applicationContext.getString(R.string.okay),
duration = SnackbarDuration.Short,
)
when (result) {
SnackbarResult.ActionPerformed,
SnackbarResult.Dismissed -> {
snackbarHostState.currentSnackbarData?.dismiss()
}
}
LaunchedEffect(appUiState.snackbarMessageConsumed) {
if (!appUiState.snackbarMessageConsumed) {
showSnackBarMessage(StringValue.DynamicString(appUiState.snackbarMessage))
appViewModel.snackbarMessageConsumed()
}
}
@@ -128,89 +167,82 @@ class MainActivity : AppCompatActivity() {
snackbarData.visuals.message,
isRtl = false,
containerColor =
MaterialTheme.colorScheme.surfaceColorAtElevation(
2.dp,
),
MaterialTheme.colorScheme.surfaceColorAtElevation(
2.dp,
),
)
}
},
//TODO refactor
modifier = Modifier
.focusable()
.focusProperties { up = focusRequester },
bottomBar =
if (notificationPermissionState == null || notificationPermissionState.status.isGranted) {
{
BottomNavBar(
navController,
listOf(
Screen.Main.navItem,
Screen.Settings.navItem,
Screen.Support.navItem,
),
)
}
} else {
{}
},
) { padding ->
Column(modifier = Modifier.padding(padding)) {
if (notificationPermissionState != null && !notificationPermissionState.status.isGranted) {
PermissionRequestFailedScreen(
onRequestAgain = {
val intentSettings =
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intentSettings.data =
Uri.fromParts(
Constants.URI_PACKAGE_SCHEME,
this@MainActivity.packageName,
null,
)
startActivity(intentSettings)
},
message = getString(R.string.notification_permission_required),
getString(R.string.open_settings),
.focusProperties { when(navBackStackEntry?.destination?.route) {
Screen.Lock.route -> Unit
else -> up = focusRequester }
},
bottomBar = {
BottomNavBar(
navController,
listOf(
Screen.Main.navItem,
Screen.Settings.navItem,
Screen.Support.navItem,
),
)
},
) { padding ->
NavHost(
navController,
startDestination =
(if (PinManager.pinExists()) Screen.Lock.route else Screen.Main.route),
modifier =
Modifier
.padding(padding)
.fillMaxSize(),
) {
composable(
Screen.Main.route,
) {
MainScreen(
focusRequester = focusRequester,
appViewModel = appViewModel,
navController = navController,
)
return@Scaffold
}
NavHost(navController, startDestination = Screen.Main.route) {
composable(
Screen.Main.route,
) {
MainScreen(
focusRequester = focusRequester,
showSnackbarMessage = { message -> showSnackBarMessage(message) },
composable(
Screen.Settings.route,
) {
SettingsScreen(
appViewModel = appViewModel,
navController = navController,
focusRequester = focusRequester
)
}
composable(
Screen.Support.route,
) {
SupportScreen(
focusRequester = focusRequester,
appViewModel = appViewModel,
navController = navController,
)
}
composable(Screen.Support.Logs.route) {
LogsScreen(appViewModel)
}
composable("${Screen.Config.route}/{id}") {
val id = it.arguments?.getString("id")
if (!id.isNullOrBlank()) {
ConfigScreen(
navController = navController,
)
}
composable(
Screen.Settings.route,
) {
SettingsScreen(
showSnackbarMessage = { message -> showSnackBarMessage(message) },
id = id,
appViewModel = appViewModel,
focusRequester = focusRequester,
)
}
composable(
Screen.Support.route,
) {
SupportScreen(
focusRequester = focusRequester,
showSnackbarMessage = { message -> showSnackBarMessage(message) },
)
}
composable("${Screen.Config.route}/{id}") {
val id = it.arguments?.getString("id")
if (!id.isNullOrBlank()) {
ConfigScreen(
navController = navController,
id = id,
showSnackbarMessage = { message ->
showSnackBarMessage(message)
},
focusRequester = focusRequester,
)
}
}
}
composable(Screen.Lock.route) {
PinLockScreen(navController = navController, appViewModel = appViewModel)
}
}
}
@@ -32,7 +32,9 @@ sealed class Screen(val route: String) {
route = route,
icon = Icons.Rounded.QuestionMark,
)
data object Logs : Screen("support/logs")
}
data object Config : Screen("config")
data object Lock : Screen("lock")
}
@@ -29,7 +29,7 @@ fun ClickableIconButton(
Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
Icon(
imageVector = icon,
contentDescription = stringResource(R.string.delete),
contentDescription = icon.name,
modifier =
Modifier.size(ButtonDefaults.IconSize).weight(1f, false).clickable {
if (enabled) {
@@ -1,36 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.common
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.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
@Composable
fun PermissionRequestFailedScreen(
onRequestAgain: () -> Unit,
message: String,
buttonText: String
) {
val scope = rememberCoroutineScope()
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxSize(),
) {
Text(message, textAlign = TextAlign.Center, modifier = Modifier.padding(15.dp))
Button(
onClick = { scope.launch { onRequestAgain() } },
) {
Text(buttonText)
}
}
}
@@ -49,7 +49,6 @@ fun RowListItem(
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(.60f),
) {
icon()
Text(text)
@@ -65,6 +64,7 @@ fun RowListItem(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly,
) {
//TODO change these to string resources
val handshakeEpoch = statistics.peer(it)!!.latestHandshakeEpochMillis
val peerTx = statistics.peer(it)!!.txBytes
val peerRx = statistics.peer(it)!!.rxBytes
@@ -6,18 +6,33 @@ import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.navigation.NavController
import androidx.navigation.compose.currentBackStackEntryAsState
import com.zaneschepke.wireguardautotunnel.ui.Screen
@Composable
fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavItem>) {
val backStackEntry = navController.currentBackStackEntryAsState()
var showBottomBar by rememberSaveable { mutableStateOf(true) }
val navBackStackEntry by navController.currentBackStackEntryAsState()
//TODO find a better way to hide nav bar
showBottomBar = when (navBackStackEntry?.destination?.route) {
Screen.Lock.route -> false
else -> true
}
NavigationBar(
containerColor = MaterialTheme.colorScheme.background,
containerColor = if(!showBottomBar) Color.Transparent else MaterialTheme.colorScheme.background,
) {
bottomNavItems.forEach { item ->
if(showBottomBar) bottomNavItems.forEach { item ->
val selected = item.route == backStackEntry.value?.destination?.route
NavigationBarItem(
@@ -49,9 +49,10 @@ fun CustomSnackBar(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start,
) {
val icon = Icons.Rounded.Info
Icon(
Icons.Rounded.Info,
contentDescription = stringResource(R.string.info),
icon,
contentDescription = icon.name,
tint = Color.White,
modifier = Modifier.padding(end = 10.dp),
)
@@ -0,0 +1,20 @@
package com.zaneschepke.wireguardautotunnel.ui.common.text
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Composable
fun LogTypeLabel(color : Color, content: @Composable () -> Unit,) {
Box(
modifier = Modifier.size(20.dp).clip(RoundedCornerShape(2.dp)).background(color), contentAlignment = Alignment.Center) {
content()
}
}
@@ -61,6 +61,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
@@ -71,6 +72,7 @@ import androidx.navigation.NavController
import com.google.accompanist.drawablepainter.DrawablePainter
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.Screen
import com.zaneschepke.wireguardautotunnel.ui.common.SearchBar
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
@@ -91,7 +93,7 @@ fun ConfigScreen(
viewModel: ConfigViewModel = hiltViewModel(),
focusRequester: FocusRequester,
navController: NavController,
showSnackbarMessage: (String) -> Unit,
appViewModel: AppViewModel,
id: String
) {
val context = LocalContext.current
@@ -125,13 +127,16 @@ fun ConfigScreen(
val fillMaxWidth = .85f
val screenPadding = 5.dp
val applicationButtonText = {
"Tunneling apps: " +
val applicationButtonText = buildAnnotatedString {
append(stringResource(id = R.string.tunneling_apps))
append(": ")
if (uiState.isAllApplicationsEnabled) {
"all"
append(stringResource(id = R.string.all))
} else {
"${uiState.checkedPackageNames.size} " +
(if (uiState.include) "included" else "excluded")
append("${uiState.checkedPackageNames.size} ")
(if (uiState.include) append(stringResource(id = R.string.included)) else append(
stringResource(id = R.string.excluded),
))
}
}
@@ -143,11 +148,11 @@ fun ConfigScreen(
},
onError = {
showAuthPrompt = false
showSnackbarMessage(Event.Error.AuthenticationFailed.message)
appViewModel.showSnackbarMessage(Event.Error.AuthenticationFailed.message)
},
onFailure = {
showAuthPrompt = false
showSnackbarMessage(Event.Error.AuthorizationFailed.message)
appViewModel.showSnackbarMessage(Event.Error.AuthorizationFailed.message)
},
)
}
@@ -164,13 +169,17 @@ fun ConfigScreen(
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface,
modifier =
Modifier.fillMaxWidth()
Modifier
.fillMaxWidth()
.fillMaxHeight(if (uiState.isAllApplicationsEnabled) 1 / 5f else 4 / 5f),
) {
Column(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier
.fillMaxWidth()) {
Row(
modifier =
Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 7.dp),
Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
@@ -183,7 +192,8 @@ fun ConfigScreen(
if (!uiState.isAllApplicationsEnabled) {
Row(
modifier =
Modifier.fillMaxWidth()
Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
@@ -215,7 +225,8 @@ fun ConfigScreen(
}
Row(
modifier =
Modifier.fillMaxWidth()
Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
@@ -232,7 +243,9 @@ fun ConfigScreen(
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxSize().padding(5.dp),
modifier = Modifier
.fillMaxSize()
.padding(5.dp),
) {
Row(modifier = Modifier.fillMaxWidth(fillMaxWidth)) {
val drawable =
@@ -244,9 +257,10 @@ fun ConfigScreen(
modifier = Modifier.size(50.dp, 50.dp),
)
} else {
val icon = Icons.Rounded.Android
Icon(
Icons.Rounded.Android,
stringResource(id = R.string.edit),
icon,
icon.name,
modifier = Modifier.size(50.dp, 50.dp),
)
}
@@ -275,7 +289,9 @@ fun ConfigScreen(
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxSize().padding(top = 5.dp),
modifier = Modifier
.fillMaxSize()
.padding(top = 5.dp),
horizontalArrangement = Arrangement.Center,
) {
TextButton(onClick = { showApplicationsDialog = false }) {
@@ -304,10 +320,10 @@ fun ConfigScreen(
viewModel.onSaveAllChanges().let {
when (it) {
is Result.Success -> {
showSnackbarMessage(it.data.message)
appViewModel.showSnackbarMessage(it.data.message)
navController.navigate(Screen.Main.route)
}
is Result.Error -> showSnackbarMessage(it.error.message)
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
}
}
},
@@ -327,7 +343,10 @@ fun ConfigScreen(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
modifier =
Modifier.verticalScroll(rememberScrollState()).weight(1f, true).fillMaxSize(),
Modifier
.verticalScroll(rememberScrollState())
.weight(1f, true)
.fillMaxSize(),
) {
Surface(
tonalElevation = 2.dp,
@@ -336,16 +355,20 @@ fun ConfigScreen(
color = MaterialTheme.colorScheme.surface,
modifier =
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Modifier.fillMaxHeight(fillMaxHeight).fillMaxWidth(fillMaxWidth)
Modifier
.fillMaxHeight(fillMaxHeight)
.fillMaxWidth(fillMaxWidth)
} else {
Modifier.fillMaxWidth(fillMaxWidth)
})
.padding(top = 50.dp, bottom = 10.dp),
.padding(bottom = 10.dp),
) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier = Modifier.padding(15.dp).focusGroup(),
modifier = Modifier
.padding(15.dp)
.focusGroup(),
) {
SectionTitle(
stringResource(R.string.interface_),
@@ -357,10 +380,14 @@ fun ConfigScreen(
keyboardActions = keyboardActions,
label = stringResource(R.string.name),
hint = stringResource(R.string.tunnel_name).lowercase(),
modifier = Modifier.fillMaxWidth().focusRequester(focusRequester),
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
OutlinedTextField(
modifier = Modifier.fillMaxWidth().clickable { showAuthPrompt = true },
modifier = Modifier
.fillMaxWidth()
.clickable { showAuthPrompt = true },
value = uiState.interfaceProxy.privateKey,
visualTransformation =
if ((id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated)
@@ -388,7 +415,9 @@ fun ConfigScreen(
)
OutlinedTextField(
modifier =
Modifier.fillMaxWidth().focusRequester(FocusRequester.Default),
Modifier
.fillMaxWidth()
.focusRequester(FocusRequester.Default),
value = uiState.interfaceProxy.publicKey,
enabled = false,
onValueChange = {},
@@ -421,7 +450,9 @@ fun ConfigScreen(
keyboardActions = keyboardActions,
label = stringResource(R.string.addresses),
hint = stringResource(R.string.comma_separated_list),
modifier = Modifier.fillMaxWidth(3 / 5f).padding(end = 5.dp),
modifier = Modifier
.fillMaxWidth(3 / 5f)
.padding(end = 5.dp),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.listenPort,
@@ -439,7 +470,9 @@ fun ConfigScreen(
keyboardActions = keyboardActions,
label = stringResource(R.string.dns_servers),
hint = stringResource(R.string.comma_separated_list),
modifier = Modifier.fillMaxWidth(3 / 5f).padding(end = 5.dp),
modifier = Modifier
.fillMaxWidth(3 / 5f)
.padding(end = 5.dp),
)
ConfigurationTextBox(
value = uiState.interfaceProxy.mtu,
@@ -452,11 +485,13 @@ fun ConfigScreen(
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxSize().padding(top = 5.dp),
modifier = Modifier
.fillMaxSize()
.padding(top = 5.dp),
horizontalArrangement = Arrangement.Center,
) {
TextButton(onClick = { showApplicationsDialog = true }) {
Text(applicationButtonText())
Text(applicationButtonText.text)
}
}
}
@@ -469,7 +504,9 @@ fun ConfigScreen(
color = MaterialTheme.colorScheme.surface,
modifier =
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Modifier.fillMaxHeight(fillMaxHeight).fillMaxWidth(fillMaxWidth)
Modifier
.fillMaxHeight(fillMaxHeight)
.fillMaxWidth(fillMaxWidth)
} else {
Modifier.fillMaxWidth(fillMaxWidth)
})
@@ -478,19 +515,24 @@ fun ConfigScreen(
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier = Modifier.padding(horizontal = 15.dp).padding(bottom = 10.dp),
modifier = Modifier
.padding(horizontal = 15.dp)
.padding(bottom = 10.dp),
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().padding(horizontal = 5.dp),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 5.dp),
) {
SectionTitle(
stringResource(R.string.peer),
padding = screenPadding,
)
IconButton(onClick = { viewModel.onDeletePeer(index) }) {
Icon(Icons.Rounded.Delete, stringResource(R.string.delete))
val icon = Icons.Rounded.Delete
Icon(icon, icon.name)
}
}
@@ -566,7 +608,9 @@ fun ConfigScreen(
Row(
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxSize().padding(bottom = 140.dp),
modifier = Modifier
.fillMaxSize()
.padding(bottom = 140.dp),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
@@ -7,8 +7,10 @@ import android.content.pm.PackageManager
import android.os.Build
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wireguard.config.BadConfigException
import com.wireguard.config.Config
import com.wireguard.config.Interface
import com.wireguard.config.ParseException
import com.wireguard.config.Peer
import com.wireguard.crypto.Key
import com.wireguard.crypto.KeyPair
@@ -30,6 +32,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@HiltViewModel
@@ -151,7 +154,7 @@ constructor(
viewModelScope.launch {
if (tunnelConfig != null) {
saveConfig(tunnelConfig).join()
WireGuardAutoTunnel.requestTileServiceStateUpdate()
WireGuardAutoTunnel.requestTileServiceStateUpdate(application)
updateSettingsDefaultTunnel(tunnelConfig)
}
}
@@ -216,7 +219,9 @@ constructor(
updateTunnelConfig(tunnelConfig)
Result.Success(Event.Message.ConfigSaved)
} catch (e: Exception) {
Result.Error(Event.Error.Exception(e))
Timber.e(e)
val message = e.message?.substringAfter(":", missingDelimiterValue = "")
Result.Error(Event.Error.ConfigParseError(message ?: ""))
}
}
@@ -1,12 +1,12 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
@@ -20,11 +20,9 @@ import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
@@ -38,6 +36,7 @@ import androidx.compose.material.icons.filled.QrCode
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.Bolt
import androidx.compose.material.icons.rounded.Circle
import androidx.compose.material.icons.rounded.CopyAll
import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material.icons.rounded.Info
@@ -50,13 +49,11 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
@@ -76,10 +73,10 @@ import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
@@ -87,12 +84,12 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions
import com.wireguard.android.backend.GoBackend
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait
import com.zaneschepke.wireguardautotunnel.ui.Screen
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
@@ -104,16 +101,18 @@ import com.zaneschepke.wireguardautotunnel.util.Event
import com.zaneschepke.wireguardautotunnel.util.Result
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
import com.zaneschepke.wireguardautotunnel.util.truncateWithEllipsis
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable
fun MainScreen(
viewModel: MainViewModel = hiltViewModel(),
appViewModel: AppViewModel,
focusRequester: FocusRequester,
showSnackbarMessage: (String) -> Unit,
navController: NavController
) {
val haptic = LocalHapticFeedback.current
@@ -129,29 +128,13 @@ fun MainScreen(
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
var vpnIntent by remember { mutableStateOf(GoBackend.VpnService.prepare(WireGuardAutoTunnel.instance)) }
val vpnActivityResultState =
rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult(),
onResult = {
val accepted = (it.resultCode == AppCompatActivity.RESULT_OK)
if (accepted) {
vpnIntent = null
}
},
)
LaunchedEffect(uiState.loading) {
if (!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) {
LaunchedEffect(Unit) {
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
delay(Constants.FOCUS_REQUEST_DELAY)
focusRequester.requestFocus()
}
}
if (uiState.loading) {
LoadingScreen()
return
}
val tunnelFileImportResultLauncher =
rememberLauncherForActivityResult(
object : ActivityResultContracts.GetContent() {
@@ -181,7 +164,7 @@ fun MainScreen(
name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB)
}
) {
showSnackbarMessage(Event.Error.FileExplorerRequired.message)
appViewModel.showSnackbarMessage(Event.Error.FileExplorerRequired.message)
}
return intent
}
@@ -191,7 +174,7 @@ fun MainScreen(
scope.launch {
viewModel.onTunnelFileSelected(data).let {
when (it) {
is Result.Error -> showSnackbarMessage(it.error.message)
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
is Result.Success -> {}
}
}
@@ -206,7 +189,7 @@ fun MainScreen(
viewModel.onTunnelQrResult(it.contents).let { result ->
when (result) {
is Result.Success -> {}
is Result.Error -> showSnackbarMessage(result.error.message)
is Result.Error -> appViewModel.showSnackbarMessage(result.error.message)
}
}
}
@@ -263,66 +246,25 @@ fun MainScreen(
}
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
if (vpnIntent != null) {
return vpnActivityResultState.launch(vpnIntent)
if(appViewModel.isRequiredPermissionGranted()) {
if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop()
}
if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop()
}
if(uiState.loading) {
return LoadingScreen()
}
Scaffold(
modifier =
Modifier.pointerInput(Unit) {
detectTapGestures(
onTap = {
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) selectedTunnel = null
},
)
},
floatingActionButtonPosition = FabPosition.End,
topBar = {
if (uiState.settings.isAutoTunnelEnabled)
TopAppBar(
title = {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier =
Modifier.requiredWidth(LocalConfiguration.current.screenWidthDp.dp)
.padding(end = 5.dp),
) {
Row {
Icon(
Icons.Rounded.Bolt,
stringResource(id = R.string.auto),
modifier = Modifier.size(25.dp),
tint =
if (uiState.settings.isAutoTunnelPaused) Color.Gray
else mint,
)
Text(
"Auto-tunneling: ${if (uiState.settings.isAutoTunnelPaused) "paused" else "active"}",
style = typography.bodyLarge,
modifier = Modifier.padding(start = 10.dp),
)
}
if (uiState.settings.isAutoTunnelPaused)
TextButton(
onClick = { viewModel.resumeAutoTunneling() },
modifier = Modifier.padding(end = 10.dp),
) {
Text("Resume")
}
else
TextButton(
onClick = { viewModel.pauseAutoTunneling() },
modifier = Modifier.padding(end = 10.dp),
) {
Text("Pause")
}
}
},
)
Modifier.pointerInput(Unit) {
detectTapGestures(
onTap = {
selectedTunnel = null
},
)
},
floatingActionButtonPosition = FabPosition.End,
floatingActionButton = {
AnimatedVisibility(
visible = isVisible.value,
@@ -334,17 +276,17 @@ fun MainScreen(
var fobColor by remember { mutableStateOf(secondaryColor) }
FloatingActionButton(
modifier =
(if (
WireGuardAutoTunnel.isRunningOnAndroidTv() &&
uiState.tunnels.isEmpty()
)
Modifier.focusRequester(focusRequester)
else Modifier)
.onFocusChanged {
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
fobColor = if (it.isFocused) hoverColor else secondaryColor
}
},
(if (
WireGuardAutoTunnel.isRunningOnAndroidTv() &&
uiState.tunnels.isEmpty()
)
Modifier.focusRequester(focusRequester)
else Modifier)
.onFocusChanged {
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
fobColor = if (it.isFocused) hoverColor else secondaryColor
}
},
onClick = { showBottomSheet = true },
containerColor = fobColor,
shape = RoundedCornerShape(16.dp),
@@ -357,12 +299,13 @@ fun MainScreen(
}
}
},
) { innerPadding ->
) {
AnimatedVisibility(uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn()) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxSize().padding(innerPadding),
modifier = Modifier
.fillMaxSize(),
) {
Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic)
}
@@ -375,12 +318,13 @@ fun MainScreen(
// Sheet content
Row(
modifier =
Modifier.fillMaxWidth()
.clickable {
showBottomSheet = false
tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES)
}
.padding(10.dp),
Modifier
.fillMaxWidth()
.clickable {
showBottomSheet = false
tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES)
}
.padding(10.dp),
) {
Icon(
Icons.Filled.FileOpen,
@@ -396,23 +340,24 @@ fun MainScreen(
HorizontalDivider()
Row(
modifier =
Modifier.fillMaxWidth()
.clickable {
scope.launch {
showBottomSheet = false
val scanOptions = ScanOptions()
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
scanOptions.setOrientationLocked(true)
scanOptions.setPrompt(
context.getString(R.string.scanning_qr)
)
scanOptions.setBeepEnabled(false)
scanOptions.captureActivity =
CaptureActivityPortrait::class.java
scanLauncher.launch(scanOptions)
}
Modifier
.fillMaxWidth()
.clickable {
scope.launch {
showBottomSheet = false
val scanOptions = ScanOptions()
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
scanOptions.setOrientationLocked(true)
scanOptions.setPrompt(
context.getString(R.string.scanning_qr),
)
scanOptions.setBeepEnabled(false)
scanOptions.captureActivity =
CaptureActivityPortrait::class.java
scanLauncher.launch(scanOptions)
}
.padding(10.dp),
}
.padding(10.dp),
) {
Icon(
Icons.Filled.QrCode,
@@ -428,14 +373,15 @@ fun MainScreen(
HorizontalDivider()
Row(
modifier =
Modifier.fillMaxWidth()
.clickable {
showBottomSheet = false
navController.navigate(
"${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}",
)
}
.padding(10.dp),
Modifier
.fillMaxWidth()
.clickable {
showBottomSheet = false
navController.navigate(
"${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}",
)
}
.padding(10.dp),
) {
Icon(
Icons.Filled.Create,
@@ -454,14 +400,62 @@ fun MainScreen(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier =
Modifier.fillMaxWidth()
.fillMaxHeight(.90f)
.overscroll(ScrollableDefaults.overscrollEffect()),
Modifier
.fillMaxWidth()
.overscroll(ScrollableDefaults.overscrollEffect()),
state = rememberLazyListState(0, uiState.tunnels.count()),
userScrollEnabled = true,
reverseLayout = true,
reverseLayout = false,
flingBehavior = ScrollableDefaults.flingBehavior(),
) {
item {
if (uiState.settings.isAutoTunnelEnabled) {
val autoTunnelingLabel = buildAnnotatedString {
append(stringResource(id = R.string.auto_tunneling))
append(": ")
if (uiState.settings.isAutoTunnelPaused) append(
stringResource(id = R.string.paused),
) else append(
stringResource(id = R.string.active),
)
}
RowListItem(
icon = {
val icon = Icons.Rounded.Bolt
Icon(
icon,
icon.name,
modifier = Modifier
.padding(end = 10.dp)
.size(25.dp),
tint =
if (uiState.settings.isAutoTunnelPaused) Color.Gray
else mint,
)
},
text = autoTunnelingLabel.text,
rowButton = {
if (uiState.settings.isAutoTunnelPaused) {
TextButton(
onClick = { viewModel.resumeAutoTunneling() },
) {
Text(stringResource(id = R.string.resume))
}
} else {
TextButton(
onClick = { viewModel.pauseAutoTunneling() },
) {
Text(stringResource(id = R.string.pause))
}
}
},
onClick = {},
onHold = {},
expanded = false,
statistics = null,
)
}
}
items(
uiState.tunnels,
key = { tunnel -> tunnel.id },
@@ -469,7 +463,7 @@ fun MainScreen(
val leadingIconColor =
(if (
uiState.vpnState.name == tunnel.name &&
uiState.vpnState.status == Tunnel.State.UP
uiState.vpnState.status == Tunnel.State.UP
) {
uiState.vpnState.statistics
?.mapPeerStats()
@@ -480,6 +474,7 @@ fun MainScreen(
statuses?.any { it == HandshakeStatus.STALE } == true -> corn
statuses?.all { it == HandshakeStatus.NOT_STARTED } == true ->
Color.Gray
else -> {
Color.Gray
}
@@ -496,24 +491,28 @@ fun MainScreen(
Icons.Rounded.Star,
stringResource(R.string.status),
tint = leadingIconColor,
modifier = Modifier.padding(end = 10.dp).size(20.dp),
modifier = Modifier
.padding(end = 10.dp)
.size(20.dp),
)
} else {
Icon(
Icons.Rounded.Circle,
stringResource(R.string.status),
tint = leadingIconColor,
modifier = Modifier.padding(end = 15.dp).size(15.dp),
modifier = Modifier
.padding(end = 15.dp)
.size(15.dp),
)
}
},
text = tunnel.name,
text = tunnel.name.truncateWithEllipsis(15),
onHold = {
if (
(uiState.vpnState.status == Tunnel.State.UP) &&
(tunnel.name == uiState.vpnState.name)
(tunnel.name == uiState.vpnState.name)
) {
showSnackbarMessage(Event.Message.TunnelOffAction.message)
appViewModel.showSnackbarMessage(Event.Message.TunnelOffAction.message)
return@RowListItem
}
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
@@ -523,7 +522,7 @@ fun MainScreen(
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
if (
uiState.vpnState.status == Tunnel.State.UP &&
(uiState.vpnState.name == tunnel.name)
(uiState.vpnState.name == tunnel.name)
) {
expanded.value = !expanded.value
}
@@ -537,7 +536,7 @@ fun MainScreen(
rowButton = {
if (
tunnel.id == selectedTunnel?.id &&
!WireGuardAutoTunnel.isRunningOnAndroidTv()
!WireGuardAutoTunnel.isRunningOnAndroidTv()
) {
Row {
if (!uiState.settings.isTunnelConfigDefault(tunnel)) {
@@ -545,9 +544,9 @@ fun MainScreen(
onClick = {
if (
uiState.settings.isAutoTunnelEnabled &&
!uiState.settings.isAutoTunnelPaused
!uiState.settings.isAutoTunnelPaused
) {
showSnackbarMessage(
appViewModel.showSnackbarMessage(
Event.Message.AutoTunnelOffAction.message,
)
} else {
@@ -555,9 +554,10 @@ fun MainScreen(
}
},
) {
val icon = Icons.Rounded.Star
Icon(
Icons.Rounded.Star,
stringResource(id = R.string.set_primary),
icon,
icon.name,
)
}
}
@@ -565,12 +565,12 @@ fun MainScreen(
onClick = {
if (
uiState.settings.isAutoTunnelEnabled &&
uiState.settings.isTunnelConfigDefault(
tunnel,
) &&
!uiState.settings.isAutoTunnelPaused
uiState.settings.isTunnelConfigDefault(
tunnel,
) &&
!uiState.settings.isAutoTunnelPaused
) {
showSnackbarMessage(
appViewModel.showSnackbarMessage(
Event.Message.AutoTunnelOffAction.message,
)
} else
@@ -579,13 +579,22 @@ fun MainScreen(
)
},
) {
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit))
val icon = Icons.Rounded.Edit
Icon(icon, icon.name)
}
IconButton(
modifier = Modifier.focusable(),
onClick = { viewModel.onCopyTunnel(selectedTunnel) },
) {
val icon = Icons.Rounded.CopyAll
Icon(icon, icon.name)
}
IconButton(
modifier = Modifier.focusable(),
onClick = { showDeleteTunnelAlertDialog = true },
) {
Icon(Icons.Rounded.Delete, stringResource(id = R.string.delete))
val icon = Icons.Rounded.Delete
Icon(icon, icon.name)
}
}
} else {
@@ -613,7 +622,7 @@ fun MainScreen(
IconButton(
onClick = {
if (uiState.settings.isAutoTunnelEnabled) {
showSnackbarMessage(
appViewModel.showSnackbarMessage(
Event.Message.AutoTunnelOffAction.message,
)
} else {
@@ -622,9 +631,10 @@ fun MainScreen(
}
},
) {
val icon = Icons.Rounded.Star
Icon(
Icons.Rounded.Star,
stringResource(id = R.string.set_primary),
icon,
icon.name,
)
}
}
@@ -633,26 +643,27 @@ fun MainScreen(
onClick = {
if (
uiState.vpnState.status == Tunnel.State.UP &&
(uiState.vpnState.name == tunnel.name)
(uiState.vpnState.name == tunnel.name)
) {
expanded.value = !expanded.value
} else {
showSnackbarMessage(
Event.Message.TunnelOnAction.message
appViewModel.showSnackbarMessage(
Event.Message.TunnelOnAction.message,
)
}
},
) {
Icon(Icons.Rounded.Info, stringResource(R.string.info))
val icon = Icons.Rounded.Info
Icon(icon, icon.name)
}
IconButton(
onClick = {
if (
uiState.vpnState.status == Tunnel.State.UP &&
tunnel.name == uiState.vpnState.name
tunnel.name == uiState.vpnState.name
) {
showSnackbarMessage(
Event.Message.TunnelOffAction.message
appViewModel.showSnackbarMessage(
Event.Message.TunnelOffAction.message,
)
} else {
navController.navigate(
@@ -661,25 +672,34 @@ fun MainScreen(
}
},
) {
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit))
val icon = Icons.Rounded.Edit
Icon(icon, icon.name)
}
IconButton(
onClick = { viewModel.onCopyTunnel(tunnel) },
) {
val icon = Icons.Rounded.CopyAll
Icon(icon, icon.name)
}
IconButton(
onClick = {
if (
uiState.vpnState.status == Tunnel.State.UP &&
tunnel.name == uiState.vpnState.name
tunnel.name == uiState.vpnState.name
) {
showSnackbarMessage(
Event.Message.TunnelOffAction.message
appViewModel.showSnackbarMessage(
Event.Message.TunnelOffAction.message,
)
} else {
selectedTunnel = tunnel
showDeleteTunnelAlertDialog = true
}
},
) {
val icon = Icons.Rounded.Delete
Icon(
Icons.Rounded.Delete,
stringResource(id = R.string.delete),
icon,
icon.name
)
}
TunnelSwitch()
@@ -7,6 +7,7 @@ import android.net.Uri
import android.provider.OpenableColumns
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.zxing.common.StringUtils
import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.model.Settings
@@ -49,7 +50,6 @@ constructor(
tunnelConfigRepository.getTunnelConfigsFlow(),
vpnService.vpnState,
) { settings, tunnels, vpnState ->
validateWatcherServiceState(settings)
MainUiState(settings, tunnels, vpnState, false)
}
.stateIn(
@@ -58,13 +58,6 @@ constructor(
MainUiState(),
)
private fun validateWatcherServiceState(settings: Settings) =
viewModelScope.launch(Dispatchers.IO) {
if (settings.isAutoTunnelEnabled) {
ServiceManager.startWatcherService(application.applicationContext)
}
}
private fun stopWatcherService() =
viewModelScope.launch(Dispatchers.IO) {
ServiceManager.stopWatcherService(application.applicationContext)
@@ -72,16 +65,17 @@ constructor(
fun onDelete(tunnel: TunnelConfig) {
viewModelScope.launch(Dispatchers.IO) {
if (tunnelConfigRepository.count() == 1) {
val settings = settingsRepository.getSettings()
val isDefault = settings.isTunnelConfigDefault(tunnel)
if (tunnelConfigRepository.count() == 1 || isDefault) {
stopWatcherService()
val settings = settingsRepository.getSettings()
settings.defaultTunnel = null
settings.isAutoTunnelEnabled = false
settings.isAlwaysOnVpnEnabled = false
saveSettings(settings)
}
tunnelConfigRepository.delete(tunnel)
WireGuardAutoTunnel.requestTileServiceStateUpdate()
WireGuardAutoTunnel.requestTileServiceStateUpdate(application)
}
}
@@ -106,7 +100,7 @@ constructor(
fun onTunnelStop() =
viewModelScope.launch(Dispatchers.IO) {
Timber.d("Stopping active tunnel")
Timber.i("Stopping active tunnel")
ServiceManager.stopVpnService(application.applicationContext)
}
@@ -122,6 +116,7 @@ constructor(
addTunnel(tunnelConfig)
Result.Success(Unit)
} catch (e: Exception) {
Timber.e(e)
Result.Error(Event.Error.InvalidQrCode)
}
}
@@ -158,6 +153,7 @@ constructor(
return Result.Error(Event.Error.InvalidFileExtension)
}
} catch (e: Exception) {
Timber.e(e)
return Result.Error(Event.Error.FileReadFailed)
}
}
@@ -190,8 +186,9 @@ constructor(
}
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
val firstTunnel = tunnelConfigRepository.count() == 0
saveTunnel(tunnelConfig)
WireGuardAutoTunnel.requestTileServiceStateUpdate()
if(firstTunnel) WireGuardAutoTunnel.requestTileServiceStateUpdate(application)
}
fun pauseAutoTunneling() =
@@ -249,6 +246,7 @@ constructor(
return try {
fileName.substring(fileName.lastIndexOf('.'))
} catch (e: Exception) {
Timber.e(e)
""
}
}
@@ -261,7 +259,13 @@ constructor(
if (selectedTunnel != null) {
saveSettings(uiState.value.settings.copy(defaultTunnel = selectedTunnel.toString()))
.join()
WireGuardAutoTunnel.requestTileServiceStateUpdate()
WireGuardAutoTunnel.requestTileServiceStateUpdate(application)
}
}
fun onCopyTunnel(tunnel: TunnelConfig?) = viewModelScope.launch {
tunnel?.let {
saveTunnel(TunnelConfig(name = it.name.plus(NumberUtils.randomThree()), wgQuick = it.wgQuick))
}
}
}
@@ -0,0 +1,49 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.pinlock
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavController
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.Screen
import com.zaneschepke.wireguardautotunnel.util.StringValue
import xyz.teamgravity.pin_lock_compose.PinLock
@Composable
fun PinLockScreen(navController: NavController, appViewModel: AppViewModel) {
val context = LocalContext.current
PinLock(
title = { pinExists ->
Text(
text = if (pinExists) stringResource(id = R.string.enter_pin) else stringResource(
id = R.string.create_pin,
),
)
},
color = MaterialTheme.colorScheme.surface,
onPinCorrect = {
// pin is correct, navigate or hide pin lock
if(WireGuardAutoTunnel.isRunningOnAndroidTv()) {
navController.navigate(Screen.Main.route)
} else {
val isPopped = navController.popBackStack()
if(!isPopped) {
navController.navigate(Screen.Main.route)
}
}
},
onPinIncorrect = {
// pin is incorrect, show error
appViewModel.showSnackbarMessage(StringValue.StringResource(R.string.incorrect_pin).asString(context))
},
onPinCreated = {
// pin created for the first time, navigate or hide pin lock
appViewModel.showSnackbarMessage(StringValue.StringResource(R.string.pin_created).asString(context))
},
)
}
@@ -21,7 +21,6 @@ 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.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -54,7 +53,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
@@ -68,6 +66,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
@@ -75,16 +74,19 @@ import com.wireguard.android.backend.Tunnel
import com.wireguard.android.backend.WgQuickBackend
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.Screen
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
import com.zaneschepke.wireguardautotunnel.util.Event
import com.zaneschepke.wireguardautotunnel.util.FileUtils
import com.zaneschepke.wireguardautotunnel.util.Result
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import timber.log.Timber
import xyz.teamgravity.pin_lock_compose.PinManager
import java.io.File
@OptIn(
@@ -94,14 +96,16 @@ import java.io.File
@Composable
fun SettingsScreen(
viewModel: SettingsViewModel = hiltViewModel(),
showSnackbarMessage: (String) -> Unit,
focusRequester: FocusRequester
appViewModel: AppViewModel,
navController: NavController,
focusRequester: FocusRequester,
) {
val scope = rememberCoroutineScope { Dispatchers.IO }
val context = LocalContext.current
val focusManager = LocalFocusManager.current
val scrollState = rememberScrollState()
val interactionSource = remember { MutableInteractionSource() }
val pinExists = remember { mutableStateOf(PinManager.pinExists()) }
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
@@ -111,19 +115,12 @@ fun SettingsScreen(
var showLocationServicesAlertDialog by remember { mutableStateOf(false) }
var didExportFiles by remember { mutableStateOf(false) }
var showAuthPrompt by remember { mutableStateOf(false) }
val focusRequester2 = remember { FocusRequester() }
val screenPadding = 5.dp
val fillMaxWidth = .85f
if (uiState.loading) {
LoadingScreen()
return
}
val startForResult =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
result: ActivityResult ->
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK) {
result.data
// Handle the Intent
@@ -139,9 +136,10 @@ fun SettingsScreen(
}
FileUtils.saveFilesToZip(context, files)
didExportFiles = true
showSnackbarMessage(Event.Message.ConfigsExported.message)
appViewModel.showSnackbarMessage(Event.Message.ConfigsExported.message)
} catch (e: Exception) {
showSnackbarMessage(Event.Error.Exception(e).message)
Timber.e(e)
appViewModel.showSnackbarMessage(Event.Error.Exception(e).message)
}
}
@@ -161,7 +159,9 @@ fun SettingsScreen(
fun handleAutoTunnelToggle() {
if (uiState.isBatteryOptimizeDisableShown || isBatteryOptimizationsDisabled()) {
viewModel.toggleAutoTunnel()
if (appViewModel.isRequiredPermissionGranted()) {
viewModel.toggleAutoTunnel()
}
} else {
requestBatteryOptimizationsDisabled()
}
@@ -172,7 +172,7 @@ fun SettingsScreen(
viewModel.onSaveTrustedSSID(currentText).let {
when (it) {
is Result.Success -> currentText = ""
is Result.Error -> showSnackbarMessage(it.error.message)
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
}
}
}
@@ -199,7 +199,7 @@ fun SettingsScreen(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (
WireGuardAutoTunnel.isRunningOnAndroidTv() &&
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q
) {
checkFineLocationGranted()
} else {
@@ -246,12 +246,16 @@ fun SettingsScreen(
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
modifier = Modifier.fillMaxSize().verticalScroll(scrollState),
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState),
) {
Icon(
Icons.Rounded.LocationOff,
contentDescription = stringResource(id = R.string.map),
modifier = Modifier.padding(30.dp).size(128.dp),
modifier = Modifier
.padding(30.dp)
.size(128.dp),
)
Text(
stringResource(R.string.prominent_background_location_title),
@@ -267,11 +271,15 @@ fun SettingsScreen(
)
Row(
modifier =
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Modifier.fillMaxWidth().padding(10.dp)
} else {
Modifier.fillMaxWidth().padding(30.dp)
},
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Modifier
.fillMaxWidth()
.padding(10.dp)
} else {
Modifier
.fillMaxWidth()
.padding(30.dp)
},
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly,
) {
@@ -299,11 +307,11 @@ fun SettingsScreen(
},
onError = { _ ->
showAuthPrompt = false
showSnackbarMessage(Event.Error.AuthenticationFailed.message)
appViewModel.showSnackbarMessage(Event.Error.AuthenticationFailed.message)
},
onFailure = {
showAuthPrompt = false
showSnackbarMessage(Event.Error.AuthorizationFailed.message)
appViewModel.showSnackbarMessage(Event.Error.AuthorizationFailed.message)
},
)
}
@@ -327,12 +335,15 @@ fun SettingsScreen(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
modifier =
Modifier.fillMaxSize().verticalScroll(scrollState).clickable(
indication = null,
interactionSource = interactionSource,
) {
focusManager.clearFocus()
},
Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.clickable(
indication = null,
interactionSource = interactionSource,
) {
focusManager.clearFocus()
},
) {
Surface(
tonalElevation = 2.dp,
@@ -340,14 +351,17 @@ fun SettingsScreen(
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface,
modifier =
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Modifier.height(IntrinsicSize.Min)
.fillMaxWidth(fillMaxWidth)
.padding(top = 10.dp)
} else {
Modifier.fillMaxWidth(fillMaxWidth).padding(top = 20.dp)
})
.padding(bottom = 10.dp),
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
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,
@@ -361,38 +375,43 @@ fun SettingsScreen(
ConfigurationToggle(
stringResource(id = R.string.tunnel_on_wifi),
enabled =
!(uiState.settings.isAutoTunnelEnabled ||
uiState.settings.isAlwaysOnVpnEnabled),
!(uiState.settings.isAutoTunnelEnabled ||
uiState.settings.isAlwaysOnVpnEnabled),
checked = uiState.settings.isTunnelOnWifiEnabled,
padding = screenPadding,
onCheckChanged = { viewModel.onToggleTunnelOnWifi() },
modifier =
if (uiState.settings.isAutoTunnelEnabled) Modifier
else
Modifier.focusRequester(focusRequester).focusProperties {
down = focusRequester2
},
if (uiState.settings.isAutoTunnelEnabled) Modifier
else
Modifier
.focusRequester(focusRequester),
)
AnimatedVisibility(visible = uiState.settings.isTunnelOnWifiEnabled) {
Column {
FlowRow(
modifier = Modifier.padding(screenPadding).fillMaxWidth(),
modifier = Modifier
.padding(screenPadding)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(5.dp),
) {
uiState.settings.trustedNetworkSSIDs.forEach { ssid ->
ClickableIconButton(
onClick = {
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
focusRequester.requestFocus()
viewModel.onDeleteTrustedSSID(ssid)
focusRequester2.requestFocus()
}
},
onIconClick = { viewModel.onDeleteTrustedSSID(ssid) },
onIconClick = {
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) focusRequester.requestFocus()
viewModel.onDeleteTrustedSSID(ssid)
},
text = ssid,
icon = Icons.Filled.Close,
enabled =
!(uiState.settings.isAutoTunnelEnabled ||
uiState.settings.isAlwaysOnVpnEnabled),
!(uiState.settings.isAutoTunnelEnabled ||
uiState.settings.isAlwaysOnVpnEnabled),
)
}
if (uiState.settings.trustedNetworkSSIDs.isEmpty()) {
@@ -405,24 +424,24 @@ fun SettingsScreen(
}
OutlinedTextField(
enabled =
!(uiState.settings.isAutoTunnelEnabled ||
uiState.settings.isAlwaysOnVpnEnabled),
!(uiState.settings.isAutoTunnelEnabled ||
uiState.settings.isAlwaysOnVpnEnabled),
value = currentText,
onValueChange = { currentText = it },
label = { Text(stringResource(R.string.add_trusted_ssid)) },
modifier =
Modifier.padding(
start = screenPadding,
top = 5.dp,
bottom = 10.dp,
)
.focusRequester(focusRequester2),
Modifier
.padding(
start = screenPadding,
top = 5.dp,
bottom = 10.dp,
),
maxLines = 1,
keyboardOptions =
KeyboardOptions(
capitalization = KeyboardCapitalization.None,
imeAction = ImeAction.Done,
),
KeyboardOptions(
capitalization = KeyboardCapitalization.None,
imeAction = ImeAction.Done,
),
keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }),
trailingIcon = {
if (currentText != "") {
@@ -430,19 +449,19 @@ fun SettingsScreen(
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,
)
},
if (currentText == "") {
stringResource(
id =
R.string
.trusted_ssid_empty_description,
)
} else {
stringResource(
id =
R.string
.trusted_ssid_value_description,
)
},
tint = MaterialTheme.colorScheme.primary,
)
}
@@ -454,8 +473,8 @@ fun SettingsScreen(
ConfigurationToggle(
stringResource(R.string.tunnel_mobile_data),
enabled =
!(uiState.settings.isAutoTunnelEnabled ||
uiState.settings.isAlwaysOnVpnEnabled),
!(uiState.settings.isAutoTunnelEnabled ||
uiState.settings.isAlwaysOnVpnEnabled),
checked = uiState.settings.isTunnelOnMobileDataEnabled,
padding = screenPadding,
onCheckChanged = { viewModel.onToggleTunnelOnMobileData() },
@@ -463,29 +482,29 @@ fun SettingsScreen(
ConfigurationToggle(
stringResource(id = R.string.tunnel_on_ethernet),
enabled =
!(uiState.settings.isAutoTunnelEnabled ||
uiState.settings.isAlwaysOnVpnEnabled),
!(uiState.settings.isAutoTunnelEnabled ||
uiState.settings.isAlwaysOnVpnEnabled),
checked = uiState.settings.isTunnelOnEthernetEnabled,
padding = screenPadding,
onCheckChanged = { viewModel.onToggleTunnelOnEthernet() },
)
ConfigurationToggle(
stringResource(R.string.battery_saver),
stringResource(R.string.restart_on_ping),
enabled =
!(uiState.settings.isAutoTunnelEnabled ||
uiState.settings.isAlwaysOnVpnEnabled),
checked = uiState.settings.isBatterySaverEnabled,
!(uiState.settings.isAutoTunnelEnabled ||
uiState.settings.isAlwaysOnVpnEnabled),
checked = uiState.settings.isPingEnabled,
padding = screenPadding,
onCheckChanged = { viewModel.onToggleBatterySaver() },
onCheckChanged = { viewModel.onToggleRestartOnPing() },
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier =
(if (!uiState.settings.isAutoTunnelEnabled) Modifier
else
Modifier.focusRequester(
focusRequester,
))
(if (!uiState.settings.isAutoTunnelEnabled) Modifier
else
Modifier.focusRequester(
focusRequester,
))
.fillMaxSize()
.padding(top = 5.dp),
horizontalArrangement = Arrangement.Center,
@@ -495,19 +514,22 @@ fun SettingsScreen(
onClick = {
if (
uiState.settings.isTunnelOnWifiEnabled &&
!uiState.settings.isAutoTunnelEnabled
!uiState.settings.isAutoTunnelEnabled
) {
when (false) {
isBackgroundLocationGranted ->
showSnackbarMessage(
Event.Error.BackgroundLocationRequired.message
appViewModel.showSnackbarMessage(
Event.Error.BackgroundLocationRequired.message,
)
fineLocationState.status.isGranted ->
showSnackbarMessage(
Event.Error.PreciseLocationRequired.message
appViewModel.showSnackbarMessage(
Event.Error.PreciseLocationRequired.message,
)
viewModel.isLocationEnabled(context) ->
showLocationServicesAlertDialog = true
else -> {
handleAutoTunnelToggle()
}
@@ -534,7 +556,9 @@ fun SettingsScreen(
shadowElevation = 2.dp,
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface,
modifier = Modifier.fillMaxWidth(fillMaxWidth).padding(vertical = 10.dp),
modifier = Modifier
.fillMaxWidth(fillMaxWidth)
.padding(vertical = 10.dp),
) {
Column(
horizontalAlignment = Alignment.Start,
@@ -548,15 +572,15 @@ fun SettingsScreen(
ConfigurationToggle(
stringResource(R.string.use_kernel),
enabled =
!(uiState.settings.isAutoTunnelEnabled ||
uiState.settings.isAlwaysOnVpnEnabled ||
(uiState.vpnState.status == Tunnel.State.UP)),
!(uiState.settings.isAutoTunnelEnabled ||
uiState.settings.isAlwaysOnVpnEnabled ||
(uiState.vpnState.status == Tunnel.State.UP)),
checked = uiState.settings.isKernelEnabled,
padding = screenPadding,
onCheckChanged = {
viewModel.onToggleKernelMode().let {
when (it) {
is Result.Error -> showSnackbarMessage(it.error.message)
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
is Result.Success -> {}
}
}
@@ -565,26 +589,27 @@ fun SettingsScreen(
}
}
}
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
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 = 140.dp),
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 = 140.dp),
) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier = Modifier.padding(15.dp),
) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier = Modifier.padding(15.dp),
) {
SectionTitle(
title = stringResource(id = R.string.other),
padding = screenPadding,
)
SectionTitle(
title = stringResource(id = R.string.other),
padding = screenPadding,
)
if(!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
ConfigurationToggle(
stringResource(R.string.always_on_vpn_support),
enabled = !uiState.settings.isAutoTunnelEnabled,
@@ -599,9 +624,27 @@ fun SettingsScreen(
padding = screenPadding,
onCheckChanged = { viewModel.onToggleShortcutsEnabled() },
)
}
ConfigurationToggle(
stringResource(R.string.enable_app_lock),
enabled = true,
checked = pinExists.value,
padding = screenPadding,
onCheckChanged = {
if (pinExists.value) {
PinManager.clearPin()
pinExists.value = PinManager.pinExists()
} else {
navController.navigate(Screen.Lock.route)
}
},
)
if(!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxSize().padding(top = 5.dp),
modifier = Modifier
.fillMaxSize()
.padding(top = 5.dp),
horizontalArrangement = Arrangement.Center,
) {
TextButton(
@@ -614,9 +657,6 @@ fun SettingsScreen(
}
}
}
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Spacer(modifier = Modifier.weight(.17f))
}
}
}
}
@@ -9,6 +9,5 @@ data class SettingsUiState(
val tunnels: List<TunnelConfig> = emptyList(),
val vpnState: VpnState = VpnState(),
val isLocationDisclosureShown: Boolean = true,
val isBatteryOptimizeDisableShown: Boolean = false,
val loading: Boolean = true
val isBatteryOptimizeDisableShown: Boolean = false
)
@@ -49,7 +49,6 @@ constructor(
tunnelState,
preferences?.get(DataStoreManager.LOCATION_DISCLOSURE_SHOWN) ?: false,
preferences?.get(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN) ?: false,
false
)
}
.stateIn(
@@ -183,9 +182,10 @@ constructor(
if (!uiState.value.settings.isKernelEnabled) {
try {
rootShell.start()
Timber.d("Root shell accepted!")
Timber.i("Root shell accepted!")
saveKernelMode(on = true)
} catch (e: RootShell.RootShellException) {
Timber.e(e)
saveKernelMode(on = false)
return Result.Error(Event.Error.RootDenied)
}
@@ -194,4 +194,12 @@ constructor(
}
return Result.Success(Unit)
}
fun onToggleRestartOnPing() = viewModelScope.launch {
saveSettings(
uiState.value.settings.copy(
isPingEnabled = !uiState.value.settings.isPingEnabled,
),
)
}
}
@@ -1,8 +1,5 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support
import android.content.Intent
import android.content.Intent.createChooser
import android.net.Uri
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Arrangement
@@ -21,6 +18,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowForward
import androidx.compose.material.icons.rounded.Book
import androidx.compose.material.icons.rounded.FormatListNumbered
import androidx.compose.material.icons.rounded.Mail
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
@@ -44,20 +42,21 @@ 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.sp
import androidx.core.content.ContextCompat.startActivity
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import com.zaneschepke.wireguardautotunnel.BuildConfig
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.Screen
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.Event
@Composable
fun SupportScreen(
viewModel: SupportViewModel = hiltViewModel(),
showSnackbarMessage: (String) -> Unit,
appViewModel: AppViewModel,
navController: NavController,
focusRequester: FocusRequester
) {
val context = LocalContext.current
@@ -65,46 +64,14 @@ fun SupportScreen(
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
fun openWebPage(url: String) {
try {
val webpage: Uri = Uri.parse(url)
val intent = Intent(Intent.ACTION_VIEW, webpage)
context.startActivity(intent)
} catch (e: Exception) {
showSnackbarMessage(Event.Error.Exception(e).message)
}
}
fun launchEmail() {
try {
val intent =
Intent(Intent.ACTION_SENDTO).apply {
type = Constants.EMAIL_MIME_TYPE
putExtra(Intent.EXTRA_EMAIL, arrayOf(context.getString(R.string.my_email)))
putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.email_subject))
}
startActivity(
context,
createChooser(intent, context.getString(R.string.email_chooser)),
null,
)
} catch (e: Exception) {
showSnackbarMessage(Event.Error.Exception(e).message)
}
}
if (uiState.loading) {
LoadingScreen()
return
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
modifier =
Modifier.fillMaxSize()
.verticalScroll(rememberScrollState())
.focusable()
Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.focusable()
) {
Surface(
tonalElevation = 2.dp,
@@ -113,15 +80,19 @@ fun SupportScreen(
color = MaterialTheme.colorScheme.surface,
modifier =
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Modifier.height(IntrinsicSize.Min)
.fillMaxWidth(fillMaxWidth)
.padding(top = 10.dp)
Modifier
.height(IntrinsicSize.Min)
.fillMaxWidth(fillMaxWidth)
.padding(top = 10.dp)
} else {
Modifier.fillMaxWidth(fillMaxWidth).padding(top = 20.dp)
Modifier
.fillMaxWidth(fillMaxWidth)
.padding(top = 20.dp)
})
.padding(bottom = 25.dp),
) {
Column(modifier = Modifier.padding(20.dp)) {
val forwardIcon = Icons.AutoMirrored.Rounded.ArrowForward
Text(
stringResource(R.string.thank_you),
textAlign = TextAlign.Start,
@@ -136,8 +107,10 @@ fun SupportScreen(
modifier = Modifier.padding(bottom = 20.dp),
)
TextButton(
onClick = { openWebPage(context.resources.getString(R.string.docs_url)) },
modifier = Modifier.padding(vertical = 5.dp).focusRequester(focusRequester),
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.docs_url)) },
modifier = Modifier
.padding(vertical = 5.dp)
.focusRequester(focusRequester),
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
@@ -145,7 +118,8 @@ fun SupportScreen(
modifier = Modifier.fillMaxWidth(),
) {
Row {
Icon(Icons.Rounded.Book, stringResource(id = R.string.docs))
val icon = Icons.Rounded.Book
Icon(icon, icon.name)
Text(
stringResource(id = R.string.docs_description),
textAlign = TextAlign.Justify,
@@ -153,8 +127,8 @@ fun SupportScreen(
)
}
Icon(
Icons.AutoMirrored.Rounded.ArrowForward,
stringResource(id = R.string.go)
forwardIcon,
forwardIcon.name
)
}
}
@@ -163,7 +137,7 @@ fun SupportScreen(
color = MaterialTheme.colorScheme.onBackground
)
TextButton(
onClick = { openWebPage(context.resources.getString(R.string.discord_url)) },
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.discord_url)) },
modifier = Modifier.padding(vertical = 5.dp),
) {
Row(
@@ -172,9 +146,10 @@ fun SupportScreen(
modifier = Modifier.fillMaxWidth(),
) {
Row {
val icon = ImageVector.vectorResource(R.drawable.discord)
Icon(
imageVector = ImageVector.vectorResource(R.drawable.discord),
stringResource(id = R.string.discord),
icon,
icon.name,
Modifier.size(25.dp),
)
Text(
@@ -184,8 +159,8 @@ fun SupportScreen(
)
}
Icon(
Icons.AutoMirrored.Rounded.ArrowForward,
stringResource(id = R.string.go)
forwardIcon,
forwardIcon.name
)
}
}
@@ -194,7 +169,7 @@ fun SupportScreen(
color = MaterialTheme.colorScheme.onBackground
)
TextButton(
onClick = { openWebPage(context.resources.getString(R.string.github_url)) },
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.github_url)) },
modifier = Modifier.padding(vertical = 5.dp),
) {
Row(
@@ -203,20 +178,21 @@ fun SupportScreen(
modifier = Modifier.fillMaxWidth(),
) {
Row {
val icon = ImageVector.vectorResource(R.drawable.github)
Icon(
imageVector = ImageVector.vectorResource(R.drawable.github),
stringResource(id = R.string.github),
imageVector = icon,
icon.name,
Modifier.size(25.dp),
)
Text(
"Open an issue",
stringResource(id = R.string.open_issue),
textAlign = TextAlign.Justify,
modifier = Modifier.padding(start = 10.dp),
)
}
Icon(
Icons.AutoMirrored.Rounded.ArrowForward,
stringResource(id = R.string.go)
forwardIcon,
forwardIcon.name
)
}
}
@@ -225,7 +201,7 @@ fun SupportScreen(
color = MaterialTheme.colorScheme.onBackground
)
TextButton(
onClick = { launchEmail() },
onClick = { appViewModel.launchEmail() },
modifier = Modifier.padding(vertical = 5.dp),
) {
Row(
@@ -234,7 +210,8 @@ fun SupportScreen(
modifier = Modifier.fillMaxWidth(),
) {
Row {
Icon(Icons.Rounded.Mail, stringResource(id = R.string.email))
val icon = Icons.Rounded.Mail
Icon(icon, icon.name)
Text(
stringResource(id = R.string.email_description),
textAlign = TextAlign.Justify,
@@ -242,11 +219,41 @@ fun SupportScreen(
)
}
Icon(
Icons.AutoMirrored.Rounded.ArrowForward,
stringResource(id = R.string.go)
forwardIcon,
forwardIcon.name
)
}
}
if(!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
HorizontalDivider(
thickness = 0.5.dp,
color = MaterialTheme.colorScheme.onBackground
)
TextButton(
onClick = { navController.navigate(Screen.Support.Logs.route) },
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))
@@ -256,7 +263,7 @@ fun SupportScreen(
fontSize = 16.sp,
modifier =
Modifier.clickable {
openWebPage(context.resources.getString(R.string.privacy_policy_url))
appViewModel.openWebPage(context.resources.getString(R.string.privacy_policy_url))
},
)
Row(
@@ -2,4 +2,4 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.support
import com.zaneschepke.wireguardautotunnel.data.model.Settings
data class SupportUiState(val settings: Settings = Settings(), val loading: Boolean = true)
data class SupportUiState(val settings: Settings = Settings())
@@ -17,7 +17,7 @@ class SupportViewModel @Inject constructor(private val settingsRepository: Setti
val uiState =
settingsRepository
.getSettingsFlow()
.map { SupportUiState(it, false) }
.map { SupportUiState(it) }
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
@@ -0,0 +1,104 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support.logs
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.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Save
import androidx.compose.material3.FloatingActionButton
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.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.common.text.LogTypeLabel
import kotlinx.coroutines.launch
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable
fun LogsScreen(appViewModel: AppViewModel) {
val logs = remember {
appViewModel.logs
}
val lazyColumnListState = rememberLazyListState()
val clipboardManager: ClipboardManager = LocalClipboardManager.current
val scope = rememberCoroutineScope()
LaunchedEffect(logs.size){
scope.launch {
lazyColumnListState.animateScrollToItem(logs.size)
}
}
Scaffold(
floatingActionButton = {
FloatingActionButton(
onClick = {
appViewModel.saveLogsToFile()
},
shape = RoundedCornerShape(16.dp),
containerColor = MaterialTheme.colorScheme.primary
) {
val icon = Icons.Filled.Save
Icon(
imageVector = icon,
contentDescription = icon.name,
tint = MaterialTheme.colorScheme.onPrimary
)
}
}
) {
LazyColumn(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top),
state = lazyColumnListState,
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 24.dp)) {
items(logs) {
Row(horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.Start), verticalAlignment = Alignment.Top,
modifier = Modifier
.fillMaxSize()
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = {
clipboardManager.setText(annotatedString = AnnotatedString(it.toString()))
}
)
) {
val fontSize = 10.sp
Text(text = it.tag, modifier = Modifier.fillMaxSize(0.3f), fontSize = fontSize)
LogTypeLabel(color = Color(it.level.color())) {
Text(text = it.level.signifier, textAlign = TextAlign.Center, fontSize = fontSize)
}
Text("${it.message} - ${it.time}", fontSize = fontSize)
}
}
}
}
}
@@ -1,17 +1,20 @@
package com.zaneschepke.wireguardautotunnel.util
object Constants {
const val BASE_LOG_FILE_NAME = "wgtunnel-logs"
const val LOG_BUFFER_SIZE = 3_000L
const val MANUAL_TUNNEL_CONFIG_ID = "0"
const val BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT = 10 * 60 * 1000L // 10 minutes
const val DEFAULT_WATCHER_WAKE_LOCK_TIMEOUT = 30 * 60 * 1000L // 30 minutes
const val VPN_STATISTIC_CHECK_INTERVAL = 1000L
const val VPN_CONNECTED_NOTIFICATION_DELAY = 3000L
const val BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT = 10 * 60 * 1_000L // 10 minutes
const val VPN_STATISTIC_CHECK_INTERVAL = 1_000L
const val VPN_CONNECTED_NOTIFICATION_DELAY = 3_000L
const val TOGGLE_TUNNEL_DELAY = 300L
const val CONF_FILE_EXTENSION = ".conf"
const val ZIP_FILE_EXTENSION = ".zip"
const val URI_CONTENT_SCHEME = "content"
const val URI_PACKAGE_SCHEME = "package"
const val ALLOWED_FILE_TYPES = "*/*"
const val TEXT_MIME_TYPE = "text/plain"
const val GOOGLE_TV_EXPLORER_STUB = "com.google.android.tv.frameworkpackagestubs"
const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs"
const val EMAIL_MIME_TYPE = "message/rfc822"
@@ -19,4 +22,11 @@ object Constants {
const val SUBSCRIPTION_TIMEOUT = 5_000L
const val FOCUS_REQUEST_DELAY = 500L
const val BACKUP_PING_HOST = "1.1.1.1"
const val PING_TIMEOUT = 5_000L
const val VPN_RESTART_DELAY = 1_000L
const val PING_INTERVAL = 60_000L
const val PING_COOLDOWN = PING_INTERVAL * 60 //one hour
}
@@ -18,6 +18,12 @@ sealed class Event {
get() = WireGuardAutoTunnel.instance.getString(R.string.error_ssid_exists)
}
data class ConfigParseError(val appendedMessage : String) : Error() {
override val message: String =
WireGuardAutoTunnel.instance.getString(R.string.config_parse_error) + (
if (appendedMessage != "") ": ${appendedMessage.trim()}" else "")
}
data object RootDenied : Error() {
override val message: String
get() = WireGuardAutoTunnel.instance.getString(R.string.error_root_denied)
@@ -31,6 +31,12 @@ fun BroadcastReceiver.goAsync(
}
}
fun String.truncateWithEllipsis(allowedLength : Int) : String {
return if(this.length > allowedLength + 3) {
this.substring(0, allowedLength) + "***"
} else this
}
fun BigDecimal.toThreeDecimalPlaceString(): String {
val df = DecimalFormat("#.###")
return df.format(this)
@@ -7,6 +7,7 @@ import android.os.Environment
import android.provider.MediaStore
import android.provider.MediaStore.MediaColumns
import java.io.File
import java.io.FileOutputStream
import java.io.OutputStream
import java.time.Instant
import java.util.zip.ZipEntry
@@ -43,6 +44,31 @@ object FileUtils {
return null
}
fun saveFileToDownloads(context: Context, content: String, fileName: String) {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
put(MediaStore.MediaColumns.MIME_TYPE, Constants.TEXT_MIME_TYPE)
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
}
val resolver = context.contentResolver
val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
if (uri != null) {
resolver.openOutputStream(uri).use { output ->
output?.write(content.toByteArray())
}
}
} else {
val target = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
fileName
)
FileOutputStream(target).use { output ->
output.write(content.toByteArray())
}
}
}
fun saveFilesToZip(context: Context, files: List<File>) {
val zipOutputStream =
createDownloadsFileOutputStream(
@@ -19,7 +19,15 @@ object NumberUtils {
}
fun generateRandomTunnelName(): String {
return "tunnel${(Math.random() * 100000).toInt()}"
return "tunnel${randomFive()}"
}
private fun randomFive() : Int {
return (Math.random() * 100000).toInt()
}
fun randomThree() : Int {
return (Math.random() * 1000).toInt()
}
fun getSecondsBetweenTimestampAndNow(epoch: Long): Long? {
@@ -0,0 +1,13 @@
package com.zaneschepke.wireguardautotunnel.util
import android.util.Log
import timber.log.Timber
class ReleaseTree : Timber.Tree() {
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
when(priority) {
Log.DEBUG -> return
}
super.log(priority,tag,message,t)
}
}
@@ -0,0 +1,24 @@
package com.zaneschepke.wireguardautotunnel.util
import android.content.Context
import androidx.annotation.StringRes
sealed class StringValue {
data class DynamicString(val value: String) : StringValue()
data object Empty : StringValue()
class StringResource(
@StringRes val resId: Int,
vararg val args: Any
) : StringValue()
fun asString(context: Context?): String {
return when (this) {
is Empty -> ""
is DynamicString -> value
is StringResource -> context?.getString(resId, *args).orEmpty()
}
}
}
+25 -12
View File
@@ -20,7 +20,7 @@
<string name="tunnel_start_title">VPN Connected</string>
<string name="tunnel_start_text">Connected to tunnel -</string>
<string name="vpn_permission_required">VPN permission is required for the app to work properly. If this permission is not launching, please disable \"Always-on VPN\" in your phone settings for the official WireGuard mobile app and try again.</string>
<string name="notification_permission_required">Notifications permission is required for the app to work properly.</string>
<string name="notification_permission_required">Notifications permission required.</string>
<string name="open_settings">Open Settings</string>
<string name="add_trusted_ssid">Add trusted wifi name</string>
<string name="tunnels">Tunnels</string>
@@ -46,8 +46,6 @@
<string name="qr_scan">QR Scan</string>
<string name="tunnel_edit">Tunnel Edit</string>
<string name="tunnel_name">Tunnel Name</string>
<string name="edit">Edit</string>
<string name="delete">Delete</string>
<string name="add_tunnel">Add Tunnel</string>
<string name="exclude">Exclude</string>
<string name="include">Include</string>
@@ -73,7 +71,7 @@
<string name="last_handshake">Last handshake</string>
<string name="name">Name</string>
<string name="restart">Restart Tunnel</string>
<string name="vpn_connection_failed">VPN Connection Failed</string>
<string name="vpn_connection_failed">Connection failed</string>
<string name="failed_connection_to">Failed connection to -</string>
<string name="initial_connection_failure_message">Attempting to connect to server after 30 seconds of no response.</string>
<string name="lost_connection_failure_message">Attempting to reconnect to server after more than one minute of no response.</string>
@@ -90,7 +88,7 @@
<string name="clear_icon">Clear Icon</string>
<string name="search_icon">Search Icon</string>
<string name="attempt_connection">Attempting connection..</string>
<string name="vpn_starting">VPN Starting</string>
<string name="vpn_starting">VPN starting</string>
<string name="db_name">wg-tunnel-db</string>
<string name="scanning_qr">Scanning for QR</string>
<string name="qr_result_failed">QR scan failed</string>
@@ -105,11 +103,9 @@
<string name="default_vpn_on">Primary VPN on</string>
<string name="default_vpn_off">Primary VPN off</string>
<string name="create_import">Create from scratch</string>
<string name="set_primary">Set primary</string>
<string name="turn_off_auto">Action requires auto-tunnel disabled</string>
<string name="turn_on_tunnel">Action requires active tunnel</string>
<string name="add_peer">Add peer</string>
<string name="info">Info</string>
<string name="done">Done</string>
<string name="interface_">Interface</string>
<string name="rotate_keys">Rotate keys</string>
@@ -119,7 +115,6 @@
<string name="comma_separated_list">comma separated list</string>
<string name="listen_port">Listen port</string>
<string name="random">(random)</string>
<string name="auto">(auto)</string>
<string name="optional">(optional)</string>
<string name="optional_no_recommend">(optional, not recommended)</string>
<string name="preshared_key">Pre-shared key</string>
@@ -146,10 +141,6 @@
<string name="go">go</string>
<string name="docs_description">Read the docs (WIP)</string>
<string name="discord_description">Join the community</string>
<string name="discord" translatable="false">Discord</string>
<string name="docs">Docs</string>
<string name="github" translatable="false">GitHub</string>
<string name="email">Email</string>
<string name="email_description">Send me an email</string>
<string name="support_help_text">If you are experiencing issues, have improvement ideas, or just want to engage, the following resources are available:</string>
<string name="kernel">Kernel</string>
@@ -167,4 +158,26 @@
<string name="delete_tunnel">Delete tunnel</string>
<string name="delete_tunnel_message">Are you sure you would like to delete this tunnel?</string>
<string name="yes">Yes</string>
<string name="resume">Resume</string>
<string name="pause">Pause</string>
<string name="paused">paused</string>
<string name="active">active</string>
<string name="tunneling_apps">Tunneling apps</string>
<string name="included">included</string>
<string name="excluded">excluded</string>
<string name="all">all</string>
<string name="always_on_disabled">Always-on VPN attempted to start a tunnel, but this feature is disabled in settings.</string>
<string name="no_email_detected">No email app detected</string>
<string name="no_browser_detected">No browser detected</string>
<string name="logs_saved">Logs saved to downloads</string>
<string name="open_issue">Open an issue</string>
<string name="read_logs">Read the logs</string>
<string name="auto">(auto)</string>
<string name="config_parse_error">Failed to parse config</string>
<string name="incorrect_pin">Pin is incorrect</string>
<string name="pin_created">Pin successfully created</string>
<string name="enter_pin">Enter your pin</string>
<string name="create_pin">Create pin</string>
<string name="enable_app_lock">Enabled app lock</string>
<string name="restart_on_ping">Restart on ping fail</string>
</resources>
+2 -10
View File
@@ -1,16 +1,8 @@
buildscript {
dependencies {
if (BuildHelper.isReleaseBuild(gradle) && BuildHelper.isGeneralFlavor(gradle)) {
classpath(libs.google.services)
classpath(libs.firebase.crashlytics.gradle)
}
}
}
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.hilt.android) apply false
kotlin("plugin.serialization").version(libs.versions.kotlin).apply(false)
alias(libs.plugins.kotlinxSerialization) apply false
alias(libs.plugins.ksp) apply false
alias(libs.plugins.androidLibrary) apply false
}
+3 -3
View File
@@ -1,12 +1,12 @@
object Constants {
const val VERSION_NAME = "3.3.6"
const val VERSION_NAME = "3.3.9"
const val JVM_TARGET = "17"
const val VERSION_CODE = 33600
const val VERSION_CODE = 33900
const val TARGET_SDK = 34
const val MIN_SDK = 26
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
const val APP_NAME = "wgtunnel"
const val COMPOSE_COMPILER_EXTENSION_VERSION = "1.5.8"
const val COMPOSE_COMPILER_EXTENSION_VERSION = "1.5.10"
const val STORE_PASS_VAR = "SIGNING_STORE_PASSWORD"
@@ -0,0 +1,2 @@
What's new:
- Tunnel display UI bug fix
@@ -0,0 +1,4 @@
What's new:
- Config edit UI bug fix
- Add GrapheneOS first launch AOVPN notification
- Bump versions
@@ -0,0 +1,5 @@
What's new:
- Add logs screen
- Add local app lock
- Add restart vpn on failed ping
- Various bug fixes
+19 -22
View File
@@ -9,29 +9,29 @@ coreKtx = "1.12.0"
datastorePreferences = "1.0.0"
desugar_jdk_libs = "2.0.4"
espressoCore = "3.5.1"
firebase-crashlytics-gradle = "2.9.9"
google-services = "4.4.1"
hiltAndroid = "2.50"
hiltNavigationCompose = "1.1.0"
hiltAndroid = "2.51"
hiltNavigationCompose = "1.2.0"
junit = "4.13.2"
kotlinx-serialization-json = "1.6.2"
kotlinx-serialization-json = "1.6.3"
lifecycle-runtime-compose = "2.7.0"
material3 = "1.2.0"
material3 = "1.2.1"
navigationCompose = "2.7.7"
pinLockCompose = "1.0.3"
roomVersion = "2.6.1"
timber = "5.0.1"
tunnel = "1.1.0"
androidGradlePlugin = "8.3.0-rc02"
tunnel = "1.0.20230706"
androidGradlePlugin = "8.3.1"
kotlin = "1.9.22"
ksp = "1.9.22-1.0.16"
composeBom = "2024.02.00"
firebaseBom = "32.7.2"
compose = "1.6.1"
crashlytics = "18.6.2"
analytics = "21.5.1"
ksp = "1.9.22-1.0.17"
composeBom = "2024.02.02"
compose = "1.6.3"
zxingAndroidEmbedded = "4.3.0"
zxingCore = "3.5.3"
#plugins
gradlePlugins-kotlinxSerialization = "1.8.21"
material = "1.11.0"
[libraries]
@@ -81,21 +81,18 @@ kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serializa
lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle-runtime-compose" }
material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "compose" }
pin-lock-compose = { module = "com.zaneschepke:pin_lock_compose", version.ref = "pinLockCompose" }
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
tunnel = { module = "com.zaneschepke:wireguard-android", version.ref = "tunnel" }
#firebase
google-firebase-crashlytics-ktx = { module = "com.google.firebase:firebase-crashlytics-ktx", version.ref = "crashlytics" }
google-firebase-analytics-ktx = { module = "com.google.firebase:firebase-analytics-ktx", version.ref = "analytics" }
firebase-crashlytics-gradle = { module = "com.google.firebase:firebase-crashlytics-gradle", version.ref = "firebase-crashlytics-gradle" }
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" }
google-services = { module = "com.google.gms:google-services", version.ref = "google-services" }
tunnel = { module = "com.wireguard.android:tunnel", version.ref = "tunnel" }
zxing-core = { module = "com.google.zxing:core", version.ref = "zxingCore" }
zxing-android-embedded = { module = "com.journeyapps:zxing-android-embedded", version.ref = "zxingAndroidEmbedded" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
[plugins]
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hiltAndroid" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
kotlinxSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "gradlePlugins-kotlinxSerialization" }
androidLibrary = { id = "com.android.library", version.ref = "androidGradlePlugin" }
+1
View File
@@ -0,0 +1 @@
/build
+43
View File
@@ -0,0 +1,43 @@
plugins {
alias(libs.plugins.androidLibrary)
alias(libs.plugins.kotlin.android)
}
android {
namespace = "com.zaneschepke.logcatter"
compileSdk = 34
defaultConfig {
minSdk = 26
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
}
View File
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,22 @@
package com.zaneschepke
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.zaneschepke.test", appContext.packageName)
}
}
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest>
</manifest>
@@ -0,0 +1,19 @@
package com.zaneschepke.logcatter
import com.zaneschepke.logcatter.model.LogMessage
object Logcatter {
fun logs(callback: (input: LogMessage) -> Unit) {
clear()
Runtime.getRuntime().exec("logcat -v epoch")
.inputStream
.bufferedReader()
.useLines { lines ->
lines.forEach { callback(LogMessage.from(it)) }
}
}
fun clear() {
Runtime.getRuntime().exec("logcat -c")
}
}
@@ -0,0 +1,48 @@
package com.zaneschepke.logcatter.model
enum class LogLevel(val signifier: String) {
DEBUG("D") {
override fun color(): Long {
return 0xFF2196F3
}
},
INFO("I"){
override fun color(): Long {
return 0xFF4CAF50
}
},
ASSERT("A"){
override fun color(): Long {
return 0xFF9C27B0
}
},
WARNING("W"){
override fun color(): Long {
return 0xFFFFC107
}
},
ERROR("E"){
override fun color(): Long {
return 0xFFF44336
}
},
VERBOSE("V"){
override fun color(): Long {
return 0xFF000000
}
};
abstract fun color() : Long
companion object {
fun fromSignifier(signifier: String) : LogLevel {
return when(signifier) {
DEBUG.signifier -> DEBUG
INFO.signifier -> INFO
WARNING.signifier -> WARNING
VERBOSE.signifier -> VERBOSE
ASSERT.signifier -> ASSERT
ERROR.signifier -> ERROR
else -> VERBOSE
}
}
}
}
@@ -0,0 +1,28 @@
package com.zaneschepke.logcatter.model
import java.time.Instant
data class LogMessage(
val time: Instant,
val pid: String,
val tid: String,
val level : LogLevel,
val tag: String,
val message: String
) {
override fun toString(): String {
return "$time $pid $tid $level $tag message= $message"
}
companion object {
fun from(logcatLine : String) : LogMessage {
return if(logcatLine.contains("---------")) LogMessage(Instant.now(), "0","0",LogLevel.VERBOSE,"System", logcatLine)
else {
//TODO improve this
val parts = logcatLine.trim().split(" ").filter { it.isNotEmpty() }
val epochParts = parts[0].split(".").map { it.toLong() }
val message = parts.subList(5, parts.size).joinToString(" ")
LogMessage(Instant.ofEpochSecond(epochParts[0], epochParts[1]), parts[1], parts[2], LogLevel.fromSignifier(parts[3]), parts[4], message)
}
}
}
}
@@ -0,0 +1,16 @@
package com.zaneschepke
import org.junit.Assert.assertEquals
import org.junit.Test
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
+4 -13
View File
@@ -1,29 +1,19 @@
pluginManagement {
repositories {
mavenLocal()
google()
mavenCentral()
mavenLocal()
gradlePluginPortal()
}
}
val GITHUB_USER_VAR = "GH_USER"
val GITHUB_TOKEN_VAR = "GH_TOKEN"
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
maven {
name = "GitHubPackages"
url = uri("https://maven.pkg.github.com/zaneschepke/wireguard-android")
credentials {
username = getLocalProperty(GITHUB_USER_VAR) ?: System.getenv(GITHUB_USER_VAR)
password = getLocalProperty(GITHUB_TOKEN_VAR) ?: System.getenv(GITHUB_TOKEN_VAR)
}
}
mavenLocal()
maven("https://gitea.zaneschepke.com/api/packages/zane/maven")
google()
mavenCentral()
mavenLocal()
}
}
@@ -41,3 +31,4 @@ fun getLocalProperty(key: String, file: String = "local.properties"): String? {
rootProject.name = "WG Tunnel"
include(":app")
include(":logcatter")