diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1d4f530d..f186ef69 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 }} diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 15ed1199..e3e280cd 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -137,7 +137,7 @@ dependencies { 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)) @@ -201,6 +201,7 @@ dependencies { // bio implementation(libs.androidx.biometric.ktx) + implementation(libs.pin.lock.compose) // shortcuts implementation(libs.androidx.core) diff --git a/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/6.json b/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/6.json new file mode 100644 index 00000000..b344c5ce --- /dev/null +++ b/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/6.json @@ -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')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt index 7f2366e9..328742b7 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt @@ -2,12 +2,14 @@ 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() { @@ -15,8 +17,8 @@ class WireGuardAutoTunnel : Application() { super.onCreate() instance = this if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) else Timber.plant(ReleaseTree()) + PinManager.initialize(this) } - companion object { lateinit var instance: WireGuardAutoTunnel private set @@ -25,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), ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt index 57934e67..29fdeef6 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt @@ -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, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/datastore/DataStoreManager.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/datastore/DataStoreManager.kt index 8af02399..2f959fe8 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/datastore/DataStoreManager.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/datastore/DataStoreManager.kt @@ -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 getFromStoreFlow(key: Preferences.Key) = context.dataStore.data.map { it[key] } suspend fun getFromStore(key: Preferences.Key) = - context.dataStore.data.first { it.contains(key) }[key] + context.dataStore.data.map{ it[key] }.first() + + fun getFromStoreBlocking(key: Preferences.Key) = runBlocking { + context.dataStore.data.map{ it[key] }.first() + } val preferencesFlow: Flow = context.dataStore.data } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/model/Settings.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/model/Settings.kt index 4aa3789c..2a8b595f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/model/Settings.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/model/Settings.kt @@ -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) { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt index 44ee1685..98894d30 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt @@ -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.i("Initiating wakelock with 10 min timeout") - acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT) - } else { - Timber.i("Initiating wakelock with 30 min 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() { @@ -201,10 +167,17 @@ class WireGuardConnectivityWatcherService : ForegroundService() { Timber.i("Starting settings watcher") watchForSettingsChanges() } + if(setting.isPingEnabled) { + launch { + Timber.i("Starting ping watcher") + watchForPingFailure() + } + } launch { Timber.i("Starting management watcher") manageVpn() } + } } @@ -236,6 +209,40 @@ class WireGuardConnectivityWatcherService : ForegroundService() { } } + 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) { @@ -331,10 +338,9 @@ class WireGuardConnectivityWatcherService : ForegroundService() { } } - // 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") } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt index e385a90a..63d81424 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt @@ -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) { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnState.kt index 730238c6..03be54d7 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnState.kt @@ -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 ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt index 80952ff8..cadcd4e5 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt @@ -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 { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppUiState.kt new file mode 100644 index 00000000..f6cf152c --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppUiState.kt @@ -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 +) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt index 36701287..81117436 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt @@ -4,13 +4,26 @@ 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 @@ -19,9 +32,32 @@ class AppViewModel constructor( private val application: Application, ) : ViewModel() { + + val vpnIntent: Intent? = GoBackend.VpnService.prepare(WireGuardAutoTunnel.instance) - private val _snackbarState = MutableStateFlow(SnackBarState()) - val snackBarState = _snackbarState.asStateFlow() + 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 { @@ -35,6 +71,12 @@ constructor( showSnackbarMessage(application.getString(R.string.no_browser_detected)) } } + + fun onVpnPermissionAccepted() { + _appUiState.value = _appUiState.value.copy( + vpnPermissionAccepted = true + ) + } fun launchEmail() { try { @@ -55,16 +97,46 @@ constructor( } } fun showSnackbarMessage(message : String) { - _snackbarState.value = _snackbarState.value.copy( + _appUiState.value = _appUiState.value.copy( snackbarMessage = message, snackbarMessageConsumed = false ) } fun snackbarMessageConsumed() { - _snackbarState.value = _snackbarState.value.copy( + _appUiState.value = _appUiState.value.copy( snackbarMessage = "", snackbarMessageConsumed = true ) } + val logs = mutableStateListOf() + + 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 + ) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt index fb1aa8b5..210d429c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt @@ -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,10 +19,14 @@ 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 @@ -35,6 +38,7 @@ 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 @@ -43,22 +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 @@ -67,9 +71,11 @@ class MainActivity : AppCompatActivity() { @Inject lateinit var dataStoreManager: DataStoreManager - @Inject lateinit var settingsRepository: SettingsRepository + @Inject + lateinit var settingsRepository: SettingsRepository + @OptIn( - ExperimentalPermissionsApi::class + ExperimentalPermissionsApi::class, ) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -78,56 +84,78 @@ 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 appViewModel = hiltViewModel() - val snackBarState by appViewModel.snackBarState.collectAsStateWithLifecycle() + val appUiState by appViewModel.appUiState.collectAsStateWithLifecycle() val navController = rememberNavController() - val focusRequester = remember { FocusRequester() } + val navBackStackEntry by navController.currentBackStackEntryAsState() - WireguardAutoTunnelTheme { + val notificationPermissionState = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) + rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS) else null - val snackbarHostState = remember { SnackbarHostState() } + val focusRequester = remember { FocusRequester() } + val snackbarHostState = remember { SnackbarHostState() } - val notificationPermissionState = if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) - rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS) else null + val vpnActivityResultState = + rememberLauncherForActivityResult( + ActivityResultContracts.StartActivityForResult(), + onResult = { + val accepted = (it.resultCode == RESULT_OK) + if (accepted) { + appViewModel.onVpnPermissionAccepted() + } + }, + ) - fun requestNotificationPermission() { + 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 ) { - notificationPermissionState.launchPermissionRequest() + 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: 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(snackBarState.snackbarMessageConsumed) { - if(!snackBarState.snackbarMessageConsumed) { - showSnackBarMessage(StringValue.DynamicString(snackBarState.snackbarMessage)) + LaunchedEffect(appUiState.snackbarMessageConsumed) { + if (!appUiState.snackbarMessageConsumed) { + showSnackBarMessage(StringValue.DynamicString(appUiState.snackbarMessage)) appViewModel.snackbarMessageConsumed() } } @@ -139,95 +167,84 @@ 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 { - {} - }, + .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 -> - if (notificationPermissionState != null && !notificationPermissionState.status.isGranted) { - Column(modifier = Modifier.padding(padding)) { - 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), + 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, + ) + } + 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, + id = id, + appViewModel = appViewModel, + focusRequester = focusRequester, ) - return@Scaffold } } - Column(modifier = Modifier.padding(padding)) { - NavHost(navController, startDestination = Screen.Main.route) { - composable( - Screen.Main.route, - ) { - MainScreen( - focusRequester = focusRequester, - appViewModel = appViewModel, - navController = navController, - ) - } - composable( - Screen.Settings.route, - ) { - SettingsScreen( - appViewModel = appViewModel, - focusRequester = focusRequester, - ) - } - composable( - Screen.Support.route, - ) { - SupportScreen( - focusRequester = focusRequester, - appViewModel = appViewModel, - navController = navController - ) - } - composable(Screen.Support.Logs.route,) { - LogsScreen() - } - composable("${Screen.Config.route}/{id}") { - val id = it.arguments?.getString("id") - if (!id.isNullOrBlank()) { - ConfigScreen( - navController = navController, - id = id, - appViewModel = appViewModel, - focusRequester = focusRequester, - ) - } - } - } + composable(Screen.Lock.route) { + PinLockScreen(navController = navController, appViewModel = appViewModel) } + } } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Screen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Screen.kt index cb7655fb..9dedd026 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Screen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Screen.kt @@ -36,4 +36,5 @@ sealed class Screen(val route: String) { } data object Config : Screen("config") + data object Lock : Screen("lock") } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/SnackBarState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/SnackBarState.kt deleted file mode 100644 index f7f129bb..00000000 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/SnackBarState.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.zaneschepke.wireguardautotunnel.ui - -data class SnackBarState( - val snackbarMessage: String = "", - val snackbarMessageConsumed: Boolean = true, -) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ClickableIconButton.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ClickableIconButton.kt index 51990dd0..31155742 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ClickableIconButton.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ClickableIconButton.kt @@ -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) { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/PermissionRequestFailedScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/PermissionRequestFailedScreen.kt deleted file mode 100644 index f6b80568..00000000 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/PermissionRequestFailedScreen.kt +++ /dev/null @@ -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) - } - } -} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/RowListItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/RowListItem.kt index 1ed4599c..54cae1ea 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/RowListItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/RowListItem.kt @@ -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 diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomNavBar.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomNavBar.kt index ef9cd207..cac6b57b 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomNavBar.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomNavBar.kt @@ -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) { 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( diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/prompt/CustomSnackbar.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/prompt/CustomSnackbar.kt index 7723f549..50d1216c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/prompt/CustomSnackbar.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/prompt/CustomSnackbar.kt @@ -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), ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt index 28637901..0e7b0355 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt @@ -257,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), ) } @@ -530,7 +531,8 @@ fun ConfigScreen( padding = screenPadding, ) IconButton(onClick = { viewModel.onDeletePeer(index) }) { - Icon(Icons.Rounded.Delete, stringResource(R.string.delete)) + val icon = Icons.Rounded.Delete + Icon(icon, icon.name) } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt index ca6ec518..ff30fa3d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt @@ -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 @@ -152,7 +154,7 @@ constructor( viewModelScope.launch { if (tunnelConfig != null) { saveConfig(tunnelConfig).join() - WireGuardAutoTunnel.requestTileServiceStateUpdate() + WireGuardAutoTunnel.requestTileServiceStateUpdate(application) updateSettingsDefaultTunnel(tunnelConfig) } } @@ -218,7 +220,8 @@ constructor( Result.Success(Event.Message.ConfigSaved) } catch (e: Exception) { Timber.e(e) - Result.Error(Event.Error.Exception(e)) + val message = e.message?.substringAfter(":", missingDelimiterValue = "") + Result.Error(Event.Error.ConfigParseError(message ?: "")) } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt index e04b9949..32f1c73e 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt @@ -7,7 +7,6 @@ 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 @@ -37,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 @@ -84,7 +84,6 @@ 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 @@ -102,6 +101,7 @@ 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 @@ -128,29 +128,13 @@ fun MainScreen( var selectedTunnel by remember { mutableStateOf(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() { @@ -262,21 +246,24 @@ 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 - }, - ) - }, + Modifier.pointerInput(Unit) { + detectTapGestures( + onTap = { + selectedTunnel = null + }, + ) + }, floatingActionButtonPosition = FabPosition.End, floatingActionButton = { AnimatedVisibility( @@ -318,7 +305,7 @@ fun MainScreen( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, modifier = Modifier - .fillMaxSize() + .fillMaxSize(), ) { Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic) } @@ -362,7 +349,7 @@ fun MainScreen( scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE) scanOptions.setOrientationLocked(true) scanOptions.setPrompt( - context.getString(R.string.scanning_qr) + context.getString(R.string.scanning_qr), ) scanOptions.setBeepEnabled(false) scanOptions.captureActivity = @@ -422,27 +409,30 @@ fun MainScreen( flingBehavior = ScrollableDefaults.flingBehavior(), ) { item { - if(uiState.settings.isAutoTunnelEnabled){ + 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) + if (uiState.settings.isAutoTunnelPaused) append( + stringResource(id = R.string.paused), ) else append( stringResource(id = R.string.active), ) } RowListItem( - icon = { Icon( - Icons.Rounded.Bolt, - stringResource(id = R.string.auto), - modifier = Modifier - .padding(end = 10.dp) - .size(25.dp), - tint = - if (uiState.settings.isAutoTunnelPaused) Color.Gray - else mint, - ) }, + 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) { @@ -457,7 +447,7 @@ fun MainScreen( ) { Text(stringResource(id = R.string.pause)) } - } + } }, onClick = {}, onHold = {}, @@ -473,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() @@ -484,6 +474,7 @@ fun MainScreen( statuses?.any { it == HandshakeStatus.STALE } == true -> corn statuses?.all { it == HandshakeStatus.NOT_STARTED } == true -> Color.Gray + else -> { Color.Gray } @@ -515,11 +506,11 @@ fun MainScreen( ) } }, - 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) ) { appViewModel.showSnackbarMessage(Event.Message.TunnelOffAction.message) return@RowListItem @@ -531,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 } @@ -545,7 +536,7 @@ fun MainScreen( rowButton = { if ( tunnel.id == selectedTunnel?.id && - !WireGuardAutoTunnel.isRunningOnAndroidTv() + !WireGuardAutoTunnel.isRunningOnAndroidTv() ) { Row { if (!uiState.settings.isTunnelConfigDefault(tunnel)) { @@ -553,7 +544,7 @@ fun MainScreen( onClick = { if ( uiState.settings.isAutoTunnelEnabled && - !uiState.settings.isAutoTunnelPaused + !uiState.settings.isAutoTunnelPaused ) { appViewModel.showSnackbarMessage( Event.Message.AutoTunnelOffAction.message, @@ -563,9 +554,10 @@ fun MainScreen( } }, ) { + val icon = Icons.Rounded.Star Icon( - Icons.Rounded.Star, - stringResource(id = R.string.set_primary), + icon, + icon.name, ) } } @@ -573,10 +565,10 @@ fun MainScreen( onClick = { if ( uiState.settings.isAutoTunnelEnabled && - uiState.settings.isTunnelConfigDefault( - tunnel, - ) && - !uiState.settings.isAutoTunnelPaused + uiState.settings.isTunnelConfigDefault( + tunnel, + ) && + !uiState.settings.isAutoTunnelPaused ) { appViewModel.showSnackbarMessage( Event.Message.AutoTunnelOffAction.message, @@ -587,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 { @@ -630,9 +631,10 @@ fun MainScreen( } }, ) { + val icon = Icons.Rounded.Star Icon( - Icons.Rounded.Star, - stringResource(id = R.string.set_primary), + icon, + icon.name, ) } } @@ -641,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 { appViewModel.showSnackbarMessage( - Event.Message.TunnelOnAction.message + 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 ) { appViewModel.showSnackbarMessage( - Event.Message.TunnelOffAction.message + Event.Message.TunnelOffAction.message, ) } else { navController.navigate( @@ -669,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 ) { appViewModel.showSnackbarMessage( - Event.Message.TunnelOffAction.message + 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() diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt index e4f21214..52d041b2 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt @@ -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) } @@ -192,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() = @@ -264,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)) + } + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/pinlock/PinLockScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/pinlock/PinLockScreen.kt new file mode 100644 index 00000000..54f9a7ca --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/pinlock/PinLockScreen.kt @@ -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)) + }, + ) +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt index 4a47fb5f..8b4c8392 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt @@ -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 @@ -76,10 +75,10 @@ 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 @@ -87,6 +86,7 @@ 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( @@ -97,13 +97,15 @@ import java.io.File fun SettingsScreen( viewModel: SettingsViewModel = hiltViewModel(), appViewModel: AppViewModel, - focusRequester: FocusRequester + 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() @@ -113,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 @@ -164,7 +159,9 @@ fun SettingsScreen( fun handleAutoTunnelToggle() { if (uiState.isBatteryOptimizeDisableShown || isBatteryOptimizationsDisabled()) { - viewModel.toggleAutoTunnel() + if (appViewModel.isRequiredPermissionGranted()) { + viewModel.toggleAutoTunnel() + } } else { requestBatteryOptimizationsDisabled() } @@ -202,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 { @@ -249,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), @@ -270,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, ) { @@ -330,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, @@ -343,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, @@ -364,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()) { @@ -408,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 != "") { @@ -433,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, ) } @@ -457,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() }, @@ -466,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, @@ -498,19 +514,22 @@ fun SettingsScreen( onClick = { if ( uiState.settings.isTunnelOnWifiEnabled && - !uiState.settings.isAutoTunnelEnabled + !uiState.settings.isAutoTunnelEnabled ) { when (false) { isBackgroundLocationGranted -> appViewModel.showSnackbarMessage( - Event.Error.BackgroundLocationRequired.message + Event.Error.BackgroundLocationRequired.message, ) + fineLocationState.status.isGranted -> appViewModel.showSnackbarMessage( - Event.Error.PreciseLocationRequired.message + Event.Error.PreciseLocationRequired.message, ) + viewModel.isLocationEnabled(context) -> showLocationServicesAlertDialog = true + else -> { handleAutoTunnelToggle() } @@ -537,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, @@ -551,9 +572,9 @@ 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 = { @@ -568,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, @@ -602,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( @@ -617,9 +657,6 @@ fun SettingsScreen( } } } - if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { - Spacer(modifier = Modifier.weight(.17f)) - } } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsUiState.kt index 01f4fe84..301039df 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsUiState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsUiState.kt @@ -9,6 +9,5 @@ data class SettingsUiState( val tunnels: List = emptyList(), val vpnState: VpnState = VpnState(), val isLocationDisclosureShown: Boolean = true, - val isBatteryOptimizeDisableShown: Boolean = false, - val loading: Boolean = true + val isBatteryOptimizeDisableShown: Boolean = false ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt index 75a68060..fff235b8 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt @@ -49,7 +49,6 @@ constructor( tunnelState, preferences?.get(DataStoreManager.LOCATION_DISCLOSURE_SHOWN) ?: false, preferences?.get(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN) ?: false, - false ) } .stateIn( @@ -195,4 +194,12 @@ constructor( } return Result.Success(Unit) } + + fun onToggleRestartOnPing() = viewModelScope.launch { + saveSettings( + uiState.value.settings.copy( + isPingEnabled = !uiState.value.settings.isPingEnabled, + ), + ) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt index f8800ca2..45cf1b94 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt @@ -64,11 +64,6 @@ fun SupportScreen( val uiState by viewModel.uiState.collectAsStateWithLifecycle() - if (uiState.loading) { - LoadingScreen() - return - } - Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Top, @@ -229,32 +224,34 @@ fun SupportScreen( ) } } - 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(), + 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 { - 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), + 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) ) } - Icon( - Icons.AutoMirrored.Rounded.ArrowForward, - stringResource(id = R.string.go) - ) } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportUiState.kt index e874416c..99bdf6af 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportUiState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportUiState.kt @@ -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()) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportViewModel.kt index dc9e638d..8b36ae4f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportViewModel.kt @@ -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), diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsScreen.kt index adac5e73..905ad54c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsScreen.kt @@ -32,26 +32,22 @@ 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(logsViewModel: LogsViewModel = hiltViewModel()) { +fun LogsScreen(appViewModel: AppViewModel) { val logs = remember { - logsViewModel.logs + appViewModel.logs } val lazyColumnListState = rememberLazyListState() val clipboardManager: ClipboardManager = LocalClipboardManager.current val scope = rememberCoroutineScope() - LaunchedEffect(Unit) { - logsViewModel.readLogCatOutput() - } - - LaunchedEffect(logs.size){ scope.launch { lazyColumnListState.animateScrollToItem(logs.size) @@ -62,7 +58,7 @@ fun LogsScreen(logsViewModel: LogsViewModel = hiltViewModel()) { floatingActionButton = { FloatingActionButton( onClick = { - logsViewModel.saveLogsToFile() + appViewModel.saveLogsToFile() }, shape = RoundedCornerShape(16.dp), containerColor = MaterialTheme.colorScheme.primary diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsViewModel.kt deleted file mode 100644 index 2625b727..00000000 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsViewModel.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.zaneschepke.wireguardautotunnel.ui.screens.support.logs - -import android.app.Application -import android.widget.Toast -import androidx.compose.runtime.mutableStateListOf -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.zaneschepke.logcatter.Logcatter -import com.zaneschepke.logcatter.model.LogMessage -import com.zaneschepke.wireguardautotunnel.R -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.launch -import java.time.Instant -import javax.inject.Inject - - - -@HiltViewModel -class LogsViewModel -@Inject -constructor( - private val application: Application -) : ViewModel() { - - val logs = mutableStateListOf() - - 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() - } - -} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt index bb813d6c..3361b099 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt @@ -7,14 +7,12 @@ object Constants { const val MANUAL_TUNNEL_CONFIG_ID = "0" const val BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT = 10 * 60 * 1_000L // 10 minutes - const val DEFAULT_WATCHER_WAKE_LOCK_TIMEOUT = 30 * 60 * 1_000L // 30 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" @@ -24,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 + } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Event.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Event.kt index d95a8836..92e626c7 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Event.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Event.kt @@ -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) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Extensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Extensions.kt index acc19ee1..9648477c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Extensions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Extensions.kt @@ -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) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/NumberUtils.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/NumberUtils.kt index 50af9eb3..d60f5101 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/NumberUtils.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/NumberUtils.kt @@ -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? { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e1ff4814..a086e682 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -20,7 +20,7 @@ VPN Connected Connected to tunnel - 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. - Notifications permission is required for the app to work properly. + Notifications permission required. Open Settings Add trusted wifi name Tunnels @@ -46,8 +46,6 @@ QR Scan Tunnel Edit Tunnel Name - Edit - Delete Add Tunnel Exclude Include @@ -105,11 +103,9 @@ Primary VPN on Primary VPN off Create from scratch - Set primary Action requires auto-tunnel disabled Action requires active tunnel Add peer - Info Done Interface Rotate keys @@ -119,7 +115,6 @@ comma separated list Listen port (random) - (auto) (optional) (optional, not recommended) Pre-shared key @@ -177,4 +172,12 @@ Logs saved to downloads Open an issue Read the logs + (auto) + Failed to parse config + Pin is incorrect + Pin successfully created + Enter your pin + Create pin + Enabled app lock + Restart on ping fail \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/Constants.kt b/buildSrc/src/main/kotlin/Constants.kt index 495ae810..54ca2d93 100644 --- a/buildSrc/src/main/kotlin/Constants.kt +++ b/buildSrc/src/main/kotlin/Constants.kt @@ -1,7 +1,7 @@ object Constants { - const val VERSION_NAME = "3.3.8-ipv6" + const val VERSION_NAME = "3.3.9" const val JVM_TARGET = "17" - const val VERSION_CODE = 33803 + const val VERSION_CODE = 33900 const val TARGET_SDK = 34 const val MIN_SDK = 26 const val APP_ID = "com.zaneschepke.wireguardautotunnel" diff --git a/fastlane/metadata/android/en-US/changelogs/33900.txt b/fastlane/metadata/android/en-US/changelogs/33900.txt new file mode 100644 index 00000000..43e104ec --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/33900.txt @@ -0,0 +1,5 @@ +What's new: +- Add logs screen +- Add local app lock +- Add restart vpn on failed ping +- Various bug fixes \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0902a672..2a887154 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,27 +9,28 @@ coreKtx = "1.12.0" datastorePreferences = "1.0.0" desugar_jdk_libs = "2.0.4" espressoCore = "3.5.1" -hiltAndroid = "2.50" +hiltAndroid = "2.51" hiltNavigationCompose = "1.2.0" junit = "4.13.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" +tunnel = "1.0.20230706" +androidGradlePlugin = "8.3.1" kotlin = "1.9.22" ksp = "1.9.22-1.0.17" -composeBom = "2024.02.01" +composeBom = "2024.02.02" compose = "1.6.3" zxingAndroidEmbedded = "4.3.0" zxingCore = "3.5.3" #plugins gradlePlugins-kotlinxSerialization = "1.8.21" -material = "1.10.0" +material = "1.11.0" [libraries] @@ -80,8 +81,9 @@ 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" } +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" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 52cf3be8..da702fd7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -7,21 +7,11 @@ pluginManagement { } } -val GITHUB_USER_VAR = "GH_USER" -val GITHUB_TOKEN_VAR = "GH_TOKEN" - dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { mavenLocal() - 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) - } - } + maven("https://gitea.zaneschepke.com/api/packages/zane/maven") google() mavenCentral() }